mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-02 19:20:09 +03:00
Remove all async/await
This commit is contained in:
parent
3154575ab6
commit
62c6565189
75
README.rst
75
README.rst
|
@ -4,70 +4,13 @@ Telethon
|
||||||
|
|
||||||
⭐️ Thanks **everyone** who has starred the project, it means a lot!
|
⭐️ Thanks **everyone** who has starred the project, it means a lot!
|
||||||
|
|
||||||
|logo| **Telethon** is an `asyncio
|
This is the threaded, simpler version of Telethon for people who
|
||||||
<https://docs.python.org/3/library/asyncio.html>`_ **Python 3** library
|
can't bother learning ``asyncio`` but wouldn't like their scripts
|
||||||
to interact with Telegram's API.
|
to just stop working. This version is also compatible with Python
|
||||||
|
3.4 but doesn't have any of the benefits of the ``asyncio`` version
|
||||||
|
and will receive updates slower.
|
||||||
|
|
||||||
**If you're upgrading from Telethon pre-1.0 to 1.0, please make sure to read**
|
Please consider learning ``asyncio``. The `documentation
|
||||||
`this section of the documentation
|
<http://telethon.rtfd.io/>`_ is the same for both versions
|
||||||
<https://telethon.readthedocs.io/en/latest/extra/basic/asyncio-magic.html>`_.
|
of the library. Simply don't write any keywords like ``async``
|
||||||
|
or ``await`` and you will be good.
|
||||||
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
|
|
||||||
|
|
||||||
pip3 install telethon
|
|
||||||
|
|
||||||
|
|
||||||
Creating a client
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
from telethon import TelegramClient, sync
|
|
||||||
|
|
||||||
# These example values won't work. You must get your own api_id and
|
|
||||||
# api_hash from https://my.telegram.org, under API Development.
|
|
||||||
api_id = 12345
|
|
||||||
api_hash = '0123456789abcdef0123456789abcdef'
|
|
||||||
|
|
||||||
client = TelegramClient('session_name', api_id, api_hash)
|
|
||||||
client.start()
|
|
||||||
|
|
||||||
|
|
||||||
Doing stuff
|
|
||||||
-----------
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
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')
|
|
||||||
messages = client.get_messages('username')
|
|
||||||
messages[0].download_media()
|
|
||||||
|
|
||||||
|
|
||||||
Next steps
|
|
||||||
----------
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
.. |logo| image:: logo.svg
|
|
||||||
:width: 24pt
|
|
||||||
:height: 24pt
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
# Minimal makefile for Sphinx documentation
|
|
||||||
#
|
|
||||||
|
|
||||||
# You can set these variables from the command line.
|
|
||||||
SPHINXOPTS =
|
|
||||||
SPHINXBUILD = sphinx-build
|
|
||||||
SPHINXPROJ = Telethon
|
|
||||||
SOURCEDIR = .
|
|
||||||
BUILDDIR = _build
|
|
||||||
|
|
||||||
# Put it first so that "make" without argument is like "make help".
|
|
||||||
help:
|
|
||||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
||||||
|
|
||||||
.PHONY: help Makefile
|
|
||||||
|
|
||||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
|
||||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
|
||||||
%: Makefile
|
|
||||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
|
@ -1,190 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
|
||||||
# Telethon documentation build configuration file, created by
|
|
||||||
# sphinx-quickstart on Fri Nov 17 15:36:11 2017.
|
|
||||||
#
|
|
||||||
# This file is execfile()d with the current directory set to its
|
|
||||||
# containing dir.
|
|
||||||
#
|
|
||||||
# Note that not all possible configuration values are present in this
|
|
||||||
# autogenerated file.
|
|
||||||
#
|
|
||||||
# All configuration values have a default; values that are commented out
|
|
||||||
# serve to show the default.
|
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
|
||||||
# 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 re
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, os.path.abspath(os.curdir))
|
|
||||||
sys.path.insert(0, os.path.abspath(os.pardir))
|
|
||||||
|
|
||||||
|
|
||||||
root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir))
|
|
||||||
|
|
||||||
tl_ref_url = 'https://lonamiwebs.github.io/Telethon'
|
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
|
||||||
#
|
|
||||||
# needs_sphinx = '1.0'
|
|
||||||
|
|
||||||
# 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',
|
|
||||||
'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']
|
|
||||||
|
|
||||||
# The suffix(es) of source filenames.
|
|
||||||
# You can specify multiple suffix as a list of string:
|
|
||||||
#
|
|
||||||
# source_suffix = ['.rst', '.md']
|
|
||||||
source_suffix = '.rst'
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
project = 'Telethon'
|
|
||||||
copyright = '2017, Lonami'
|
|
||||||
author = 'Lonami'
|
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
|
||||||
# |version| and |release|, also used in various other places throughout the
|
|
||||||
# built documents.
|
|
||||||
#
|
|
||||||
# The short X.Y version.
|
|
||||||
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 = version
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
||||||
# for a list of supported languages.
|
|
||||||
#
|
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
|
||||||
# Usually you set "language" from the command line for these cases.
|
|
||||||
language = None
|
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
|
||||||
# directories to ignore when looking for source files.
|
|
||||||
# This patterns also effect to html_static_path and html_extra_path
|
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'sphinx'
|
|
||||||
|
|
||||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
|
||||||
todo_include_todos = False
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ----------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
||||||
# a list of builtin themes.
|
|
||||||
#
|
|
||||||
html_theme = 'sphinx_rtd_theme'
|
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
|
||||||
# further. For a list of options available for each theme, see the
|
|
||||||
# documentation.
|
|
||||||
#
|
|
||||||
html_theme_options = {
|
|
||||||
'collapse_navigation': True,
|
|
||||||
'display_version': True,
|
|
||||||
'navigation_depth': 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
||||||
html_static_path = ['_static']
|
|
||||||
|
|
||||||
# Custom sidebar templates, must be a dictionary that maps document names
|
|
||||||
# to template names.
|
|
||||||
#
|
|
||||||
# This is required for the alabaster theme
|
|
||||||
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
|
|
||||||
html_sidebars = {
|
|
||||||
'**': [
|
|
||||||
'globaltoc.html',
|
|
||||||
'relations.html', # needs 'show_related': True theme option to display
|
|
||||||
'searchbox.html',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTMLHelp output ------------------------------------------
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = 'Telethondoc'
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output ---------------------------------------------
|
|
||||||
|
|
||||||
latex_elements = {
|
|
||||||
# The paper size ('letterpaper' or 'a4paper').
|
|
||||||
#
|
|
||||||
# 'papersize': 'letterpaper',
|
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
|
||||||
#
|
|
||||||
# 'pointsize': '10pt',
|
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
|
||||||
#
|
|
||||||
# 'preamble': '',
|
|
||||||
|
|
||||||
# Latex figure (float) alignment
|
|
||||||
#
|
|
||||||
# 'figure_align': 'htbp',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title,
|
|
||||||
# author, documentclass [howto, manual, or own class]).
|
|
||||||
latex_documents = [
|
|
||||||
(master_doc, 'Telethon.tex', 'Telethon Documentation',
|
|
||||||
author, 'manual'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for manual page output ---------------------------------------
|
|
||||||
|
|
||||||
# One entry per manual page. List of tuples
|
|
||||||
# (source start file, name, description, authors, manual section).
|
|
||||||
man_pages = [
|
|
||||||
(master_doc, 'telethon', 'Telethon Documentation',
|
|
||||||
[author], 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Texinfo output -------------------------------------------
|
|
||||||
|
|
||||||
# Grouping the document tree into Texinfo files. List of tuples
|
|
||||||
# (source start file, target name, title, author,
|
|
||||||
# dir menu entry, description, category)
|
|
||||||
texinfo_documents = [
|
|
||||||
(master_doc, 'Telethon', 'Telethon Documentation',
|
|
||||||
author, 'Telethon', 'One line description of project.',
|
|
||||||
'Miscellaneous'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
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
|
|
|
@ -1,162 +0,0 @@
|
||||||
.. _accessing-the-full-api:
|
|
||||||
|
|
||||||
======================
|
|
||||||
Accessing the Full API
|
|
||||||
======================
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
While you have access to this, you should always use the friendly
|
|
||||||
methods listed on :ref:`telethon-client` unless you have a better
|
|
||||||
reason not to, like a method not existing or you wanting more control.
|
|
||||||
|
|
||||||
|
|
||||||
The :ref:`TelegramClient <telethon-client>` 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.
|
|
||||||
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
All the examples in this documentation assume that you have
|
|
||||||
``from telethon import sync`` or ``import telethon.sync``
|
|
||||||
for the sake of simplicity and that you understand what
|
|
||||||
it does (see :ref:`asyncio-magic` for more). Simply add
|
|
||||||
either line at the beginning of your project and it will 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
|
|
||||||
<telethon.client.messages.MessageMethods.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 `client.get_input_entity
|
|
||||||
<telethon.client.users.UserMethods.get_input_entity>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import telethon.sync
|
|
||||||
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
|
|
||||||
`client.get_input_entity <telethon.client.users.UserMethods.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 `client.get_entity <telethon.client.users.UserMethods.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 `telethon.utils.get_input_peer`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import utils
|
|
||||||
peer = utils.get_input_peer(entity)
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Since ``v0.16.2`` this is further simplified. The ``Request`` itself
|
|
||||||
will call `client.get_input_entity <
|
|
||||||
telethon.client.users.UserMethods.get_input_entity>` for you when required,
|
|
||||||
but it's good to remember what's happening.
|
|
||||||
|
|
||||||
|
|
||||||
After this small parenthesis about `client.get_entity
|
|
||||||
<telethon.client.users.UserMethods.get_entity>` versus
|
|
||||||
`client.get_input_entity <telethon.client.users.UserMethods.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 over 250
|
|
||||||
methods available as of layer 80, 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&redirect=no
|
|
|
@ -1,134 +0,0 @@
|
||||||
.. _sessions:
|
|
||||||
|
|
||||||
==============
|
|
||||||
Session Files
|
|
||||||
==============
|
|
||||||
|
|
||||||
The first parameter you pass to the constructor of the
|
|
||||||
:ref:`TelegramClient <telethon-client>` 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 in the working directory.
|
|
||||||
|
|
||||||
Note that if you pass a string it will be a file in the current working
|
|
||||||
directory, although you can also pass absolute paths.
|
|
||||||
|
|
||||||
The session file contains enough information for you to login without
|
|
||||||
re-sending the code, so if you have to enter the code more than once,
|
|
||||||
maybe you're changing the working directory, renaming or removing the
|
|
||||||
file, or using random names.
|
|
||||||
|
|
||||||
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
|
|
||||||
:ref:`TelegramClient <telethon-client>` instead of
|
|
||||||
the session name.
|
|
||||||
|
|
||||||
Telethon contains two implementations of the abstract ``Session`` class:
|
|
||||||
|
|
||||||
* ``MemorySession``: stores session data in Python variables.
|
|
||||||
* ``SQLiteSession``, (default): stores sessions in their own SQLite databases.
|
|
||||||
|
|
||||||
There are other community-maintained implementations available:
|
|
||||||
|
|
||||||
* `SQLAlchemy <https://github.com/tulir/telethon-session-sqlalchemy>`_: stores all sessions in a single database via SQLAlchemy.
|
|
||||||
* `Redis <https://github.com/ezdev128/telethon-session-redis>`_: stores all sessions in a single Redis data store.
|
|
||||||
|
|
||||||
Creating your own storage
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The easiest way to create your own storage implementation is to use ``MemorySession``
|
|
||||||
as the base and check out how ``SQLiteSession`` or one of the community-maintained
|
|
||||||
implementations work. You can find the relevant Python files under the ``sessions``
|
|
||||||
directory in Telethon.
|
|
||||||
|
|
||||||
After you have made your own implementation, you can add it to the community-maintained
|
|
||||||
session implementation list above with a pull request.
|
|
||||||
|
|
||||||
SQLite Sessions and Heroku
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
You probably have a newer version of SQLite installed (>= 3.8.2). Heroku uses
|
|
||||||
SQLite 3.7.9 which does not support ``WITHOUT ROWID``. So, if you generated
|
|
||||||
your session file on a system with SQLite >= 3.8.2 your session file will not
|
|
||||||
work on Heroku's platform and will throw a corrupted schema error.
|
|
||||||
|
|
||||||
There are multiple ways to solve this, the easiest of which is generating a
|
|
||||||
session file on your Heroku dyno itself. The most complicated is creating
|
|
||||||
a custom buildpack to install SQLite >= 3.8.2.
|
|
||||||
|
|
||||||
|
|
||||||
Generating a SQLite Session File on a Heroku Dyno
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
Due to Heroku's ephemeral filesystem all dynamically generated
|
|
||||||
files not part of your applications buildpack or codebase are destroyed
|
|
||||||
upon each restart.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
Do not restart your application Dyno at any point prior to retrieving your
|
|
||||||
session file. Constantly creating new session files from Telegram's API
|
|
||||||
will result in a 24 hour rate limit ban.
|
|
||||||
|
|
||||||
Due to Heroku's ephemeral filesystem all dynamically generated
|
|
||||||
files not part of your applications buildpack or codebase are destroyed upon
|
|
||||||
each restart.
|
|
||||||
|
|
||||||
Using this scaffolded code we can start the authentication process:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client = TelegramClient('login.session', api_id, api_hash).start()
|
|
||||||
|
|
||||||
At this point your Dyno will crash because you cannot access stdin. Open your
|
|
||||||
Dyno's control panel on the Heroku website and "Run console" from the "More"
|
|
||||||
dropdown at the top right. Enter ``bash`` and wait for it to load.
|
|
||||||
|
|
||||||
You will automatically be placed into your applications working directory.
|
|
||||||
So run your application ``python app.py`` and now you can complete the input
|
|
||||||
requests such as "what is your phone number" etc.
|
|
||||||
|
|
||||||
Once you're successfully authenticated exit your application script with
|
|
||||||
CTRL + C and ``ls`` to confirm ``login.session`` exists in your current
|
|
||||||
directory. Now you can create a git repo on your account and commit
|
|
||||||
``login.session`` to that repo.
|
|
||||||
|
|
||||||
You cannot ``ssh`` into your Dyno instance because it has crashed, so unless
|
|
||||||
you programatically upload this file to a server host this is the only way to
|
|
||||||
get it off of your Dyno.
|
|
||||||
|
|
||||||
You now have a session file compatible with SQLite <= 3.8.2. Now you can
|
|
||||||
programatically fetch this file from an external host (Firebase, S3 etc.)
|
|
||||||
and login to your session using the following scaffolded code:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
fileName, headers = urllib.request.urlretrieve(file_url, 'login.session')
|
|
||||||
client = TelegramClient(os.path.abspath(fileName), api_id, api_hash).start()
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
- ``urlretrieve`` will be depreciated, consider using ``requests``.
|
|
||||||
- ``file_url`` represents the location of your file.
|
|
|
@ -1,65 +0,0 @@
|
||||||
.. _update-modes:
|
|
||||||
|
|
||||||
============
|
|
||||||
Update Modes
|
|
||||||
============
|
|
||||||
|
|
||||||
With ``asyncio``, the library has several tasks running in the background.
|
|
||||||
One task is used for sending requests, another task is used to receive them,
|
|
||||||
and a third one is used to handle updates.
|
|
||||||
|
|
||||||
To handle updates, you must keep your script running. You can do this in
|
|
||||||
several ways. For instance, if you are *not* running ``asyncio``'s event
|
|
||||||
loop, you should use `client.run_until_disconnected
|
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from telethon import TelegramClient
|
|
||||||
|
|
||||||
client = TelegramClient(...)
|
|
||||||
...
|
|
||||||
client.run_until_disconnected()
|
|
||||||
|
|
||||||
|
|
||||||
Behind the scenes, this method is ``await``'ing on the `client.disconnected
|
|
||||||
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>` property,
|
|
||||||
so the code above and the following are equivalent:
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from telethon import TelegramClient
|
|
||||||
|
|
||||||
client = TelegramClient(...)
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
await client.disconnected
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
|
|
||||||
You could also run `client.disconnected
|
|
||||||
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>`
|
|
||||||
until it completed.
|
|
||||||
|
|
||||||
But if you don't want to ``await``, then you should know what you want
|
|
||||||
to be doing instead! What matters is that you shouldn't let your script
|
|
||||||
die. If you don't care about updates, you don't need any of this.
|
|
||||||
|
|
||||||
Notice that unlike `client.disconnected
|
|
||||||
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>`,
|
|
||||||
`client.run_until_disconnected
|
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` will
|
|
||||||
handle ``KeyboardInterrupt`` with you. This method is special and can
|
|
||||||
also be ran while the loop is running, so you can do this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
await client.run_until_disconnected()
|
|
||||||
|
|
||||||
loop.run_until_complete(main())
|
|
|
@ -1,321 +0,0 @@
|
||||||
.. _asyncio-magic:
|
|
||||||
|
|
||||||
==================
|
|
||||||
Magic with asyncio
|
|
||||||
==================
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
TL; DR; If you've upgraded to Telethon 1.0 from a previous version
|
|
||||||
**and you're not using events or updates**, add this line:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import telethon.sync
|
|
||||||
|
|
||||||
At the beginning of your main script and you will be good. If you do use
|
|
||||||
updates or events, keep reading, or install the latest version using
|
|
||||||
threads and Python 3.4 support with ``pip install telethon==0.19.1.6``.
|
|
||||||
|
|
||||||
You might also want to check the :ref:`changelog`.
|
|
||||||
|
|
||||||
|
|
||||||
The sync module
|
|
||||||
***************
|
|
||||||
|
|
||||||
It's time to tell you the truth. The library has been doing magic behind
|
|
||||||
the scenes. We're sorry to tell you this, but at least it wasn't dark magic!
|
|
||||||
|
|
||||||
You may have noticed one of these lines across the documentation:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import sync
|
|
||||||
# or
|
|
||||||
import telethon.sync
|
|
||||||
|
|
||||||
Either of these lines will import the *magic* ``sync`` module. When you
|
|
||||||
import this module, you can suddenly use all the methods defined in the
|
|
||||||
:ref:`TelegramClient <telethon-client>` like so:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_message('me', 'Hello!')
|
|
||||||
|
|
||||||
for dialog in client.iter_dialogs():
|
|
||||||
print(dialog.title)
|
|
||||||
|
|
||||||
|
|
||||||
What happened behind the scenes is that all those methods, called *coroutines*,
|
|
||||||
were rewritten to be normal methods that will block (with some exceptions).
|
|
||||||
This means you can use the library without worrying about ``asyncio`` and
|
|
||||||
event loops.
|
|
||||||
|
|
||||||
However, this only works until you run the event loop yourself explicitly:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
async def coro():
|
|
||||||
client.send_message('me', 'Hello!') # <- no longer works!
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(coro())
|
|
||||||
|
|
||||||
|
|
||||||
What things will work and when?
|
|
||||||
*******************************
|
|
||||||
|
|
||||||
You can use all the methods in the :ref:`TelegramClient <telethon-client>`
|
|
||||||
in a synchronous, blocking way without trouble, as long as you're not running
|
|
||||||
the loop as we saw above (the ``loop.run_until_complete(...)`` line runs "the
|
|
||||||
loop"). If you're running the loop, then *you* are the one responsible to
|
|
||||||
``await`` everything. So to fix the code above:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
async def coro():
|
|
||||||
await client.send_message('me', 'Hello!')
|
|
||||||
# ^ notice this new await
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(coro())
|
|
||||||
|
|
||||||
The library can only run the loop until the method completes if the loop
|
|
||||||
isn't already running, which is why the magic can't work if you run the
|
|
||||||
loop yourself.
|
|
||||||
|
|
||||||
**When you work with updates or events**, the loop needs to be
|
|
||||||
running one way or another (using `client.run_until_disconnected()
|
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` runs the loop),
|
|
||||||
so your event handlers must be ``async def``.
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
Turning your event handlers into ``async def`` is the biggest change
|
|
||||||
between Telethon pre-1.0 and 1.0, but updating will likely cause a
|
|
||||||
noticeable speed-up in your programs. Keep reading!
|
|
||||||
|
|
||||||
|
|
||||||
So in short, you can use **all** methods in the client with ``await`` or
|
|
||||||
without it if the loop isn't running:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_message('me', 'Hello!') # works
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
await client.send_message('me', 'Hello!') # also works
|
|
||||||
|
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
|
|
||||||
When you work with updates, you should stick using the ``async def main``
|
|
||||||
way, since your event handlers will be ``async def`` too.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
There are two exceptions. Both `client.run_until_disconnected()
|
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` and
|
|
||||||
`client.start() <telethon.client.auth.AuthMethods.start>` work in
|
|
||||||
and outside of ``async def`` for convenience without importing the
|
|
||||||
magic module. The rest of methods remain ``async`` unless you import it.
|
|
||||||
|
|
||||||
You can skip the rest if you already know how ``asyncio`` works and you
|
|
||||||
already understand what the magic does and how it works. Just remember
|
|
||||||
to ``await`` all your methods if you're inside an ``async def`` or are
|
|
||||||
using updates and you will be good.
|
|
||||||
|
|
||||||
|
|
||||||
Why asyncio?
|
|
||||||
************
|
|
||||||
|
|
||||||
Python's `asyncio <https://docs.python.org/3/library/asyncio.html>`_ is the
|
|
||||||
standard way to run asynchronous code from within Python. Since Python 3.5,
|
|
||||||
using ``async def`` and ``await`` became possible, and Python 3.6 further
|
|
||||||
improves what you can do with asynchronous code, although it's not the only
|
|
||||||
way (other projects like `Trio <https://github.com/python-trio>`_ also exist).
|
|
||||||
|
|
||||||
Telegram is a service where all API calls are executed in an asynchronous
|
|
||||||
way. You send your request, and eventually, Telegram will process it and
|
|
||||||
respond to it. It feels natural to make a library that also behaves this
|
|
||||||
way: you send a request, and you can ``await`` for its result.
|
|
||||||
|
|
||||||
Now that we know that Telegram's API follows an asynchronous model, you
|
|
||||||
should understand the benefits of developing a library that does the same,
|
|
||||||
it greatly simplifies the internal code and eases working with the API.
|
|
||||||
|
|
||||||
Using ``asyncio`` keeps a cleaner library that will be easier to understand,
|
|
||||||
develop, and that will be faster than using threads, which are harder to get
|
|
||||||
right and can cause issues. It also enables to use the powerful ``asyncio``
|
|
||||||
system such as futures, timeouts, cancellation, etc. in a natural way.
|
|
||||||
|
|
||||||
If you're still not convinced or you're just not ready for using ``asyncio``,
|
|
||||||
the library offers a synchronous interface without the need for all the
|
|
||||||
``async`` and ``await`` you would otherwise see. `Follow this link
|
|
||||||
<https://github.com/LonamiWebs/Telethon/tree/sync>`_ to find out more.
|
|
||||||
|
|
||||||
|
|
||||||
How do I get started?
|
|
||||||
*********************
|
|
||||||
|
|
||||||
To get started with ``asyncio``, all you need is to setup your main
|
|
||||||
``async def`` like so:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
pass # Your code goes here
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
You don't need to ``import telethon.sync`` if you're going to work this
|
|
||||||
way. This is the best way to work in real programs since the loop won't
|
|
||||||
be starting and ending all the time, but is a bit more annoying to setup.
|
|
||||||
|
|
||||||
Inside ``async def main()``, you can use the ``await`` keyword. Most
|
|
||||||
methods in the :ref:`TelegramClient <telethon-client>` are ``async def``.
|
|
||||||
You must ``await`` all ``async def``, also known as a *coroutines*:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
client = TelegramClient(...)
|
|
||||||
|
|
||||||
# client.start() is a coroutine (async def), it needs an await
|
|
||||||
await client.start()
|
|
||||||
|
|
||||||
# Sending a message also interacts with the API, and needs an await
|
|
||||||
await client.send_message('me', 'Hello myself!')
|
|
||||||
|
|
||||||
|
|
||||||
If you don't know anything else about ``asyncio``, this will be enough
|
|
||||||
to get you started. Once you're ready to learn more about it, you will
|
|
||||||
be able to use that power and everything you've learnt with Telethon.
|
|
||||||
Just remember that if you use ``await``, you need to be inside of an
|
|
||||||
``async def``.
|
|
||||||
|
|
||||||
Another way to use ``async def`` is to use ``loop.run_until_complete(f())``,
|
|
||||||
but the loop must not be running before.
|
|
||||||
|
|
||||||
If you want to handle updates (and don't let the script die), you must
|
|
||||||
`await client.run_until_disconnected()
|
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>`
|
|
||||||
which is a property that you can wait on until you call
|
|
||||||
`await client.disconnect()
|
|
||||||
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`:
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client = TelegramClient(...)
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async def handler(event):
|
|
||||||
print(event)
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
await client.start()
|
|
||||||
await client.run_until_disconnected()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
`client.run_until_disconnected()
|
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` and
|
|
||||||
`client.start()
|
|
||||||
<telethon.client.auth.AuthMethods.start>` are special-cased and work
|
|
||||||
inside or outside ``async def`` for convenience, even without importing
|
|
||||||
the ``sync`` module, so you can also do this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client = TelegramClient(...)
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async def handler(event):
|
|
||||||
print(event)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
client.start()
|
|
||||||
client.run_until_disconnected()
|
|
||||||
|
|
||||||
|
|
||||||
Which methods should I use and when?
|
|
||||||
************************************
|
|
||||||
|
|
||||||
Something to note is that you must always get an event loop if you
|
|
||||||
want to be able to make any API calls. This is done as follows:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
The loop must be running, or things will never get sent.
|
|
||||||
Normally, you use ``run_until_complete``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def coroutine():
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
loop.run_until_complete(coroutine())
|
|
||||||
|
|
||||||
Note that ``asyncio.sleep`` is in itself a coroutine, so this will
|
|
||||||
work too:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
loop.run_until_complete(asyncio.sleep(1))
|
|
||||||
|
|
||||||
Generally, you make an ``async def main()`` if you need to ``await``
|
|
||||||
a lot of things, instead of typing ``run_until_complete`` all the time:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
message = await client.send_message('me', 'Hi')
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
await message.delete()
|
|
||||||
|
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
# vs
|
|
||||||
|
|
||||||
message = loop.run_until_complete(client.send_message('me', 'Hi'))
|
|
||||||
loop.run_until_complete(asyncio.sleep(1))
|
|
||||||
loop.run_until_complete(message.delete())
|
|
||||||
|
|
||||||
You can see that the first version has more lines, but you had to type
|
|
||||||
a lot less. You can also rename the run method to something shorter:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# Note no parenthesis (), we're not running it, just copying the method
|
|
||||||
rc = loop.run_until_complete
|
|
||||||
message = rc(client.send_message('me', 'Hi'))
|
|
||||||
rc(asyncio.sleep(1))
|
|
||||||
rc(message.delete())
|
|
||||||
|
|
||||||
The documentation generally runs the loop until complete behind the
|
|
||||||
scenes if you've imported the magic ``sync`` module, but if you haven't,
|
|
||||||
you need to run the loop yourself. We recommend that you use the
|
|
||||||
``async def main()`` method to do all your work with ``await``.
|
|
||||||
It's the easiest and most performant thing to do.
|
|
||||||
|
|
||||||
|
|
||||||
More resources to learn asyncio
|
|
||||||
*******************************
|
|
||||||
|
|
||||||
If you would like to learn a bit more about why ``asyncio`` is something
|
|
||||||
you should learn, `check out my blog post
|
|
||||||
<https://lonamiwebs.github.io/blog/asyncio/>`_ that goes into more detail.
|
|
|
@ -1,228 +0,0 @@
|
||||||
.. _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.
|
|
||||||
|
|
||||||
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 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!
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import TelegramClient, sync
|
|
||||||
|
|
||||||
# Use your own values here
|
|
||||||
api_id = 12345
|
|
||||||
api_hash = '0123456789abcdef0123456789abcdef'
|
|
||||||
|
|
||||||
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 by default a database file using Python's ``sqlite3``.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
It's important that the library always accesses the same session file so
|
|
||||||
that you don't need to re-send the code over and over again. By default it
|
|
||||||
creates the file in your working directory, but absolute paths work too.
|
|
||||||
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
The process shown here shows how to sign in *manually*. You **should**
|
|
||||||
use `client.start() <telethon.client.auth.AuthMethods.start>` instead
|
|
||||||
unless you have a better reason not to (e.g. you need more control):
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.start()
|
|
||||||
|
|
||||||
This is explained after going through the manual process.
|
|
||||||
|
|
||||||
|
|
||||||
Before using the client, you must be connected to Telegram.
|
|
||||||
Doing so is very easy:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.connect()
|
|
||||||
|
|
||||||
You may or may not be authorized yet. You must be authorized
|
|
||||||
before you're able to send any request:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.is_user_authorized() # Returns True if you can send requests
|
|
||||||
|
|
||||||
If you're not authorized, you need to `.sign_in
|
|
||||||
<telethon.client.auth.AuthMethods.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.
|
|
||||||
|
|
||||||
.. 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)
|
|
||||||
|
|
||||||
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()
|
|
||||||
<telethon.client.auth.AuthMethods.start>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client = TelegramClient('anon', api_id, api_hash)
|
|
||||||
client.start()
|
|
||||||
|
|
||||||
|
|
||||||
The code shown is just what `.start()
|
|
||||||
<telethon.client.auth.AuthMethods.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()
|
|
||||||
<telethon.client.users.UserMethods.get_me>`.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
Please note that if you fail to login around 5 times (or change the first
|
|
||||||
parameter of the :ref:`TelegramClient <telethon-client>`, 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:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import socks
|
|
||||||
client = TelegramClient('session_id',
|
|
||||||
api_id=12345, api_hash='0123456789abcdef0123456789abcdef',
|
|
||||||
proxy=(socks.SOCKS5, 'localhost', 4444)
|
|
||||||
)
|
|
||||||
|
|
||||||
The ``proxy=`` argument should be a tuple, a list or a dict,
|
|
||||||
consisting of parameters described `here`__.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Two Factor Authorization (2FA)
|
|
||||||
******************************
|
|
||||||
|
|
||||||
If you have Two Factor Authorization (from now on, 2FA) enabled on your
|
|
||||||
account, calling `.sign_in()
|
|
||||||
<telethon.client.auth.AuthMethods.sign_in>` will raise a
|
|
||||||
``SessionPasswordNeededError``. When this happens, just use the method
|
|
||||||
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()
|
|
||||||
<telethon.client.auth.AuthMethods.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()
|
|
||||||
<telethon.client.auth.AuthMethods.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
|
|
|
@ -1,161 +0,0 @@
|
||||||
.. _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.
|
|
||||||
|
|
||||||
To "encounter" an ID, you would have to "find it" like you would in the
|
|
||||||
normal app. If the peer is in your dialogs, you would need to
|
|
||||||
`client.get_dialogs() <telethon.client.dialogs.DialogMethods.get_dialogs>`.
|
|
||||||
If the peer is someone in a group, you would similarly
|
|
||||||
`client.get_participants(group) <telethon.client.chats.ChatMethods.get_participants>`.
|
|
||||||
|
|
||||||
Once you have encountered an ID, the library will (by default) have saved
|
|
||||||
their ``access_hash`` for you, which is needed to invoke most methods.
|
|
||||||
This is why sometimes you might encounter this error when working with
|
|
||||||
the library. You should ``except ValueError`` and run code that you know
|
|
||||||
should work to find the entity.
|
|
||||||
|
|
||||||
|
|
||||||
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()
|
|
||||||
<telethon.client.users.UserMethods.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!')
|
|
||||||
<telethon.client.messages.MessageMethods.send_message>`
|
|
||||||
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 `client.get_entity() <telethon.client.users.UserMethods.get_entity>`
|
|
||||||
beforehand, just use the username or phone, or the entity retrieved by
|
|
||||||
other means like `client.get_dialogs()
|
|
||||||
<telethon.client.dialogs.DialogMethods.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,
|
|
||||||
`client.get_input_entity() <telethon.client.users.UserMethods.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**
|
|
||||||
`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`
|
|
||||||
**over**
|
|
||||||
`client.get_entity() <telethon.client.users.UserMethods.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 :tl:`InputPeer`. Only use
|
|
||||||
`client.get_entity() <telethon.client.users.UserMethods.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
|
|
||||||
`client.get_input_entity() <telethon.client.users.UserMethods.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.
|
|
||||||
|
|
||||||
|
|
||||||
Full entities
|
|
||||||
*************
|
|
||||||
|
|
||||||
In addition to :tl:`PeerUser`, :tl:`InputPeerUser`, :tl:`User` (and its
|
|
||||||
variants for chats and channels), there is also the concept of :tl:`UserFull`.
|
|
||||||
|
|
||||||
This full variant has additional information such as whether the user is
|
|
||||||
blocked, its notification settings, the bio or about of the user, etc.
|
|
||||||
|
|
||||||
There is also :tl:`messages.ChatFull` which is the equivalent of full entities
|
|
||||||
for chats and channels, with also the about section of the channel. Note that
|
|
||||||
the ``users`` field only contains bots for the channel (so that clients can
|
|
||||||
suggest commands to use).
|
|
||||||
|
|
||||||
You can get both of these by invoking :tl:`GetFullUser`, :tl:`GetFullChat`
|
|
||||||
and :tl:`GetFullChannel` respectively.
|
|
|
@ -1,93 +0,0 @@
|
||||||
.. _getting-started:
|
|
||||||
|
|
||||||
|
|
||||||
===============
|
|
||||||
Getting Started
|
|
||||||
===============
|
|
||||||
|
|
||||||
|
|
||||||
Simple Installation
|
|
||||||
*******************
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
pip3 install telethon
|
|
||||||
|
|
||||||
**More details**: :ref:`installation`
|
|
||||||
|
|
||||||
|
|
||||||
Creating a client
|
|
||||||
*****************
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import TelegramClient, sync
|
|
||||||
|
|
||||||
# These example values won't work. You must get your own api_id and
|
|
||||||
# api_hash from https://my.telegram.org, under API Development.
|
|
||||||
api_id = 12345
|
|
||||||
api_hash = '0123456789abcdef0123456789abcdef'
|
|
||||||
|
|
||||||
client = TelegramClient('session_name', api_id, api_hash).start()
|
|
||||||
|
|
||||||
**More details**: :ref:`creating-a-client`
|
|
||||||
|
|
||||||
|
|
||||||
Basic Usage
|
|
||||||
***********
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# Getting information about yourself
|
|
||||||
me = client.get_me()
|
|
||||||
print(me.stringify())
|
|
||||||
|
|
||||||
# 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')
|
|
||||||
|
|
||||||
# Retrieving messages from a chat
|
|
||||||
from telethon import utils
|
|
||||||
for message in client.iter_messages('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(dialog.name, dialog.draft.text)
|
|
||||||
|
|
||||||
# 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(),
|
|
||||||
# or even using message.download_media():
|
|
||||||
messages = client.get_messages('username')
|
|
||||||
messages[0].download_media()
|
|
||||||
|
|
||||||
**More details**: :ref:`telegram-client`
|
|
||||||
|
|
||||||
See :ref:`telethon-client` for all available friendly methods.
|
|
||||||
|
|
||||||
|
|
||||||
Handling Updates
|
|
||||||
****************
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import events
|
|
||||||
|
|
||||||
@client.on(events.NewMessage(incoming=True, pattern='(?i)hi'))
|
|
||||||
def handler(event):
|
|
||||||
event.reply('Hello!')
|
|
||||||
|
|
||||||
client.run_until_disconnected()
|
|
||||||
|
|
||||||
**More details**: :ref:`working-with-updates`
|
|
||||||
|
|
||||||
|
|
||||||
----------
|
|
||||||
|
|
||||||
You can continue by clicking on the "More details" link below each
|
|
||||||
snippet of code or the "Next" button at the bottom of the page.
|
|
|
@ -1,97 +0,0 @@
|
||||||
.. _installation:
|
|
||||||
|
|
||||||
============
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
|
|
||||||
Automatic Installation
|
|
||||||
**********************
|
|
||||||
|
|
||||||
To install Telethon, simply do:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
pip3 install telethon
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
pip3 install --upgrade telethon
|
|
||||||
|
|
||||||
You can also install the library directly from GitHub or a fork:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
# 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 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:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
pip3 install telethon[cryptg]
|
|
||||||
|
|
||||||
|
|
||||||
Manual Installation
|
|
||||||
*******************
|
|
||||||
|
|
||||||
1. Install the required ``pyaes`` (`GitHub`__ | `PyPi`__) and
|
|
||||||
``rsa`` (`GitHub`__ | `PyPi`__) modules:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
pip3 install pyaes rsa
|
|
||||||
|
|
||||||
2. Clone Telethon's GitHub repository:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
git clone https://github.com/LonamiWebs/Telethon.git
|
|
||||||
|
|
||||||
3. Enter the cloned repository:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
cd Telethon
|
|
||||||
|
|
||||||
4. Run the code generator:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
python3 setup.py gen
|
|
||||||
|
|
||||||
5. Done!
|
|
||||||
|
|
||||||
To generate the `method documentation`__, ``python3 setup.py gen docs``.
|
|
||||||
|
|
||||||
|
|
||||||
Optional dependencies
|
|
||||||
*********************
|
|
||||||
|
|
||||||
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://pypi.python.org/pypi/rsa/3.4.2
|
|
||||||
__ https://lonamiwebs.github.io/Telethon
|
|
||||||
__ https://github.com/Lonami/cryptg
|
|
|
@ -1,108 +0,0 @@
|
||||||
.. _telegram-client:
|
|
||||||
|
|
||||||
==============
|
|
||||||
TelegramClient
|
|
||||||
==============
|
|
||||||
|
|
||||||
|
|
||||||
Introduction
|
|
||||||
************
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Make sure to use the friendly methods described in :ref:`telethon-client`!
|
|
||||||
This section is just an introduction to using the client, but all the
|
|
||||||
available methods are in the :ref:`telethon-client` reference, including
|
|
||||||
detailed descriptions to what they do.
|
|
||||||
|
|
||||||
The :ref:`TelegramClient <telethon-client>` 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 :ref:`TelegramClient <telethon-client>` 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
|
|
||||||
(assuming you have ``from telethon import sync`` or ``import telethon.sync``):
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
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!')
|
|
||||||
|
|
||||||
# .send_message's parse mode defaults to markdown, so you
|
|
||||||
# can use **bold**, __italics__, [links](https://example.com), `code`,
|
|
||||||
# and even [mentions](@username)/[mentions](tg://user?id=123456789)
|
|
||||||
client.send_message('username', '**Using** __markdown__ `too`!')
|
|
||||||
|
|
||||||
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.iter_messages('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(dialog.name, dialog.draft.text)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
*****************
|
|
||||||
|
|
||||||
The :ref:`reference <telethon-package>` lists all the "handy" methods
|
|
||||||
available for you to use in the :ref:`TelegramClient <telethon-client>` 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.
|
|
||||||
|
|
||||||
See the mentioned :ref:`telethon-client` to find the available methods.
|
|
||||||
|
|
||||||
.. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py
|
|
||||||
.. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py
|
|
|
@ -1,273 +0,0 @@
|
||||||
.. _working-with-updates:
|
|
||||||
|
|
||||||
====================
|
|
||||||
Working with Updates
|
|
||||||
====================
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
Make sure you have read at least the first part of :ref:`asyncio-magic`
|
|
||||||
before working with updates. **This is a big change from Telethon pre-1.0
|
|
||||||
and 1.0, and your old handlers won't work with this version**.
|
|
||||||
|
|
||||||
To port your code to the new version, you should just prefix all your
|
|
||||||
event handlers with ``async`` and ``await`` everything that makes an
|
|
||||||
API call, such as replying, deleting messages, etc.
|
|
||||||
|
|
||||||
|
|
||||||
The library comes with the `telethon.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!
|
|
||||||
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
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 when working with events, at least the error level, to see if
|
|
||||||
this is happening so you can debug the error.
|
|
||||||
|
|
||||||
**When using updates, please enable logging:**
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logging.basicConfig(level=logging.ERROR)
|
|
||||||
|
|
||||||
|
|
||||||
.. contents::
|
|
||||||
|
|
||||||
|
|
||||||
Getting Started
|
|
||||||
***************
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import TelegramClient, events
|
|
||||||
|
|
||||||
client = TelegramClient('name', api_id, api_hash)
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async def my_event_handler(event):
|
|
||||||
if 'hello' in event.raw_text:
|
|
||||||
await event.reply('hi!')
|
|
||||||
|
|
||||||
client.start()
|
|
||||||
client.run_until_disconnected()
|
|
||||||
|
|
||||||
|
|
||||||
Not much, but there might be some things unclear. What does this code do?
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import TelegramClient, events
|
|
||||||
|
|
||||||
client = TelegramClient('name', api_id, api_hash)
|
|
||||||
|
|
||||||
|
|
||||||
This is normal creation (of course, pass session name, API ID and hash).
|
|
||||||
Nothing we don't know already.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
|
|
||||||
|
|
||||||
This Python decorator will attach itself to the ``my_event_handler``
|
|
||||||
definition, and basically means that *on* a `NewMessage
|
|
||||||
<telethon.events.newmessage.NewMessage>` *event*,
|
|
||||||
the callback function you're about to define will be called:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def my_event_handler(event):
|
|
||||||
if 'hello' in event.raw_text:
|
|
||||||
await event.reply('hi!')
|
|
||||||
|
|
||||||
|
|
||||||
If a `NewMessage
|
|
||||||
<telethon.events.newmessage.NewMessage>` event occurs,
|
|
||||||
and ``'hello'`` is in the text of the message, we `.reply()
|
|
||||||
<telethon.tl.custom.message.Message.reply>` to the event
|
|
||||||
with a ``'hi!'`` message.
|
|
||||||
|
|
||||||
Do you notice anything different? Yes! Event handlers **must** be ``async``
|
|
||||||
for them to work, and **every method using the network** needs to have an
|
|
||||||
``await``, otherwise, Python's ``asyncio`` will tell you that you forgot
|
|
||||||
to do so, so you can easily add it.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.start()
|
|
||||||
client.run_until_disconnected()
|
|
||||||
|
|
||||||
|
|
||||||
Finally, this tells the client that we're done with our code. We run the
|
|
||||||
``asyncio`` loop until the client starts (this is done behind the scenes,
|
|
||||||
since the method is so common), and then we run it again until we are
|
|
||||||
disconnected. Of course, you can do other things instead of running
|
|
||||||
until disconnected. For this refer to :ref:`update-modes`.
|
|
||||||
|
|
||||||
|
|
||||||
More on events
|
|
||||||
**************
|
|
||||||
|
|
||||||
The `NewMessage <telethon.events.newmessage.NewMessage>` event has much
|
|
||||||
more than what was shown. You can access the `.sender
|
|
||||||
<telethon.tl.custom.message.Message.sender>` of the message
|
|
||||||
through that member, or even see if the message had `.media
|
|
||||||
<telethon.tl.custom.message.Message.media>`, a `.photo
|
|
||||||
<telethon.tl.custom.message.Message.photo>` or a `.document
|
|
||||||
<telethon.tl.custom.message.Message.document>` (which you
|
|
||||||
could download with for example `client.download_media(event.photo)
|
|
||||||
<telethon.client.downloads.DownloadMethods.download_media>`.
|
|
||||||
|
|
||||||
If you don't want to `.reply()
|
|
||||||
<telethon.tl.custom.message.Message.reply>` as a reply,
|
|
||||||
you can use the `.respond() <telethon.tl.custom.message.Message.respond>`
|
|
||||||
method instead. Of course, there are more events such as `ChatAction
|
|
||||||
<telethon.events.chataction.ChatAction>` or `UserUpdate
|
|
||||||
<telethon.events.userupdate.UserUpdate>`, and they're all
|
|
||||||
used in the same way. Simply add the `@client.on(events.XYZ)
|
|
||||||
<telethon.client.updates.UpdateMethods.on>` 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
|
|
||||||
<telethon.events.newmessage.NewMessage.Event>`), except for the `Raw
|
|
||||||
<telethon.events.raw.Raw>` event which just passes the :tl:`Update` object.
|
|
||||||
|
|
||||||
Note that `.reply()
|
|
||||||
<telethon.tl.custom.message.Message.reply>` and `.respond()
|
|
||||||
<telethon.tl.custom.message.Message.respond>` are just wrappers around the
|
|
||||||
`client.send_message() <telethon.client.messages.MessageMethods.send_message>`
|
|
||||||
method which supports the ``file=`` parameter.
|
|
||||||
This means you can reply with a photo if you do `event.reply(file=photo)
|
|
||||||
<telethon.tl.custom.message.Message.reply>`.
|
|
||||||
|
|
||||||
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')))
|
|
||||||
async def normal_handler(event):
|
|
||||||
if 'roll' in event.raw_text:
|
|
||||||
await 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,
|
|
||||||
pattern='eval (.+)'))
|
|
||||||
async def admin_handler(event):
|
|
||||||
expression = event.pattern_match.group(1)
|
|
||||||
await 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!
|
|
||||||
|
|
||||||
|
|
||||||
Properties vs. methods
|
|
||||||
**********************
|
|
||||||
|
|
||||||
The event shown above acts just like a `custom.Message
|
|
||||||
<telethon.tl.custom.message.Message>`, which means you
|
|
||||||
can access all the properties it has, like ``.sender``.
|
|
||||||
|
|
||||||
**However** events are different to other methods in the client, like
|
|
||||||
`client.get_messages <telethon.client.messages.MessageMethods.get_messages>`.
|
|
||||||
Events *may not* send information about the sender or chat, which means it
|
|
||||||
can be ``None``, but all the methods defined in the client always have this
|
|
||||||
information so it doesn't need to be re-fetched. For this reason, you have
|
|
||||||
``get_`` methods, which will make a network call if necessary.
|
|
||||||
|
|
||||||
In short, you should do this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async def handler(event):
|
|
||||||
# event.input_chat may be None, use event.get_input_chat()
|
|
||||||
chat = await event.get_input_chat()
|
|
||||||
sender = await event.get_sender()
|
|
||||||
buttons = await event.get_buttons()
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async for message in client.iter_messages('me', 10):
|
|
||||||
# Methods from the client always have these properties ready
|
|
||||||
chat = message.input_chat
|
|
||||||
sender = message.sender
|
|
||||||
buttons = message.buttons
|
|
||||||
|
|
||||||
Notice, properties (`message.sender
|
|
||||||
<telethon.tl.custom.message.Message.sender>`) don't need an ``await``, but
|
|
||||||
methods (`message.get_sender
|
|
||||||
<telethon.tl.custom.message.Message.get_sender>`) **do** need an ``await``,
|
|
||||||
and you should use methods in events for these properties that may need network.
|
|
||||||
|
|
||||||
|
|
||||||
Events without decorators
|
|
||||||
*************************
|
|
||||||
|
|
||||||
If for any reason you can't use the `@client.on
|
|
||||||
<telethon.client.updates.UpdateMethods.on>` syntax, don't worry.
|
|
||||||
You can call `client.add_event_handler(callback, event)
|
|
||||||
<telethon.client.updates.UpdateMethods.add_event_handler>` to achieve
|
|
||||||
the same effect.
|
|
||||||
|
|
||||||
Similarly, you also have `client.remove_event_handler
|
|
||||||
<telethon.client.updates.UpdateMethods.remove_event_handler>`
|
|
||||||
and `client.list_event_handlers
|
|
||||||
<telethon.client.updates.UpdateMethods.list_event_handlers>`.
|
|
||||||
|
|
||||||
The ``event`` type is optional in all methods and defaults to
|
|
||||||
`events.Raw <telethon.events.raw.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 `telethon.events.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)
|
|
||||||
async def _(event):
|
|
||||||
# ... some conditions
|
|
||||||
await event.delete()
|
|
||||||
|
|
||||||
# Other handlers won't have an event to work with
|
|
||||||
raise StopPropagation
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async 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://lonamiwebs.github.io/Telethon/types/update.html
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,54 +0,0 @@
|
||||||
.. _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
|
|
|
@ -1,22 +0,0 @@
|
||||||
============
|
|
||||||
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``!
|
|
|
@ -1,25 +0,0 @@
|
||||||
==========
|
|
||||||
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.
|
|
|
@ -1,52 +0,0 @@
|
||||||
=================
|
|
||||||
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 from several mixing ``Method`` classes,
|
|
||||||
since there are so many methods that having them in a single file would
|
|
||||||
make maintenance painful (it was three thousand lines before this separation
|
|
||||||
happened!). It's a "god object", but there is only a way to interact with
|
|
||||||
Telegram really.
|
|
||||||
|
|
||||||
The ``TelegramBaseClient`` is an ABC which will support all of these mixins
|
|
||||||
so they can work together nicely. It doesn't even know how to invoke things
|
|
||||||
because they need to be resolved with user information first (to work with
|
|
||||||
input entities comfortably).
|
|
||||||
|
|
||||||
The client makes use of the ``network/mtprotosender.py``. The
|
|
||||||
``MTProtoSender`` is responsible for connecting, reconnecting,
|
|
||||||
packing, unpacking, sending and receiving items from the network.
|
|
||||||
Basically, the low-level communication with Telegram, and handling
|
|
||||||
MTProto-related functions and types such as ``BadSalt``.
|
|
||||||
|
|
||||||
The sender makes use of a ``Connection`` class which knows the format in
|
|
||||||
which outgoing messages should be sent (how to encode their length and
|
|
||||||
their body, if they're further encrypted).
|
|
||||||
|
|
||||||
For now, all connection modes make use of the ``extensions/tcpclient``,
|
|
||||||
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.
|
|
||||||
|
|
||||||
Auto-generated code
|
|
||||||
*******************
|
|
||||||
|
|
||||||
The files under ``telethon_generator/`` are used to generate the code
|
|
||||||
that gets placed under ``telethon/tl/``. The parsers take in files in
|
|
||||||
a specific format (such as ``.tl`` for objects and ``.json`` for errors)
|
|
||||||
and spit 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.
|
|
||||||
|
|
||||||
Custom documentation can also be generated to easily navigate through
|
|
||||||
the vast amount of items offered by the API.
|
|
|
@ -1,73 +0,0 @@
|
||||||
===============================
|
|
||||||
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>`__.
|
|
|
@ -1,37 +0,0 @@
|
||||||
============
|
|
||||||
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)
|
|
||||||
loop.run_until_complete(client.start(
|
|
||||||
phone='9996621234', code_callback=lambda: '22222'
|
|
||||||
))
|
|
|
@ -1,17 +0,0 @@
|
||||||
============================
|
|
||||||
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!
|
|
|
@ -1,33 +0,0 @@
|
||||||
===============================
|
|
||||||
Understanding the Type Language
|
|
||||||
===============================
|
|
||||||
|
|
||||||
|
|
||||||
`Telegram's Type Language <https://core.telegram.org/mtproto/TL>`__
|
|
||||||
(also known as TL, found on ``.tl`` files) is a concise way to define
|
|
||||||
what other programming languages commonly call classes or structs.
|
|
||||||
|
|
||||||
Every definition is written as follows for a Telegram object is defined
|
|
||||||
as follows:
|
|
||||||
|
|
||||||
``name#id argument_name:argument_type = CommonType``
|
|
||||||
|
|
||||||
This means that in a single line you know what the ``TLObject`` name is.
|
|
||||||
You know it's unique ID, and you know what arguments it has. It really
|
|
||||||
isn't that hard to write a generator for generating code to any
|
|
||||||
platform!
|
|
||||||
|
|
||||||
The generated code should also be able to *encode* the ``TLObject`` (let
|
|
||||||
this be a request or a type) into bytes, so they can be sent over the
|
|
||||||
network. This isn't a big deal either, because you know how the
|
|
||||||
``TLObject``\ 's are made, and how the types should be serialized.
|
|
||||||
|
|
||||||
You can either write your own code generator, or use the one this
|
|
||||||
library provides, but please be kind and keep some special mention to
|
|
||||||
this project for helping you out.
|
|
||||||
|
|
||||||
This is only a introduction. The ``TL`` language is not *that* easy. But
|
|
||||||
it's not that hard either. You're free to sniff the
|
|
||||||
``telethon_generator/`` files and learn how to parse other more complex
|
|
||||||
lines, such as ``flags`` (to indicate things that may or may not be
|
|
||||||
written at all) and ``vector``\ 's.
|
|
|
@ -1,71 +0,0 @@
|
||||||
====
|
|
||||||
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
|
|
||||||
:tl:`GetInlineBotResultsRequest` request:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.messages import GetInlineBotResultsRequest
|
|
||||||
|
|
||||||
bot_results = client(GetInlineBotResultsRequest(
|
|
||||||
bot, user_or_chat, 'query', ''
|
|
||||||
))
|
|
||||||
|
|
||||||
And you can select any of their results by using
|
|
||||||
:tl:`SendInlineBotResultRequest`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.messages import SendInlineBotResultRequest
|
|
||||||
|
|
||||||
client(SendInlineBotResultRequest(
|
|
||||||
get_input_peer(user_or_chat),
|
|
||||||
obtained_query_id,
|
|
||||||
obtained_str_id
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
Talking to Bots with special reply markup
|
|
||||||
*****************************************
|
|
||||||
|
|
||||||
Generally, you just use the `message.click()
|
|
||||||
<telethon.tl.custom.message.Message.click>` method:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
messages = client.get_messages('somebot')
|
|
||||||
messages[0].click(0)
|
|
||||||
|
|
||||||
You can also do it manually.
|
|
||||||
|
|
||||||
To interact with a message that has a special reply markup, such as
|
|
||||||
`@VoteBot`__ polls, you would use :tl:`GetBotCallbackAnswerRequest`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.messages import GetBotCallbackAnswerRequest
|
|
||||||
|
|
||||||
client(GetBotCallbackAnswerRequest(
|
|
||||||
user_or_chat,
|
|
||||||
msg.id,
|
|
||||||
data=msg.reply_markup.rows[wanted_row].buttons[wanted_button].data
|
|
||||||
))
|
|
||||||
|
|
||||||
It's a bit verbose, but it has all the information you would need to
|
|
||||||
show it visually (button rows, and buttons within each row, each with
|
|
||||||
its own data).
|
|
||||||
|
|
||||||
__ https://t.me/vote
|
|
||||||
__ https://t.me/vote
|
|
|
@ -1,318 +0,0 @@
|
||||||
===============================
|
|
||||||
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 :tl:`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 :tl:`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`__.
|
|
||||||
|
|
||||||
|
|
||||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
:tl:`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 :tl:`AddChatUserRequest`, which
|
|
||||||
use is very straightforward, or :tl:`InviteToChannelRequest` for channels:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# For normal chats
|
|
||||||
from telethon.tl.functions.messages import AddChatUserRequest
|
|
||||||
|
|
||||||
# Note that ``user_to_add`` is NOT the name of the parameter.
|
|
||||||
# It's the user you want to add (``user_id=user_to_add``).
|
|
||||||
client(AddChatUserRequest(
|
|
||||||
chat_id,
|
|
||||||
user_to_add,
|
|
||||||
fwd_limit=10 # Allow the user to see the 10 last messages
|
|
||||||
))
|
|
||||||
|
|
||||||
# For channels (which includes megagroups)
|
|
||||||
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 :tl:`CheckChatInviteRequest`, which takes in
|
|
||||||
the hash of said channel or group.
|
|
||||||
|
|
||||||
|
|
||||||
Retrieving all chat members (channels too)
|
|
||||||
******************************************
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Use the `telethon.telegram_client.TelegramClient.iter_participants`
|
|
||||||
friendly method instead unless you have a better reason not to!
|
|
||||||
|
|
||||||
This method will handle different chat types for you automatically.
|
|
||||||
|
|
||||||
|
|
||||||
Here is the easy way to do it:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
participants = client.get_participants(group)
|
|
||||||
|
|
||||||
Now we will show how the method works internally.
|
|
||||||
|
|
||||||
In order to get all the members from a mega-group or channel, you need
|
|
||||||
to use :tl:`GetParticipantsRequest`. As we can see it needs an
|
|
||||||
:tl:`InputChannel`, (passing the mega-group or channel you're going to
|
|
||||||
use will work), and a mandatory :tl:`ChannelParticipantsFilter`. The
|
|
||||||
closest thing to "no filter" is to simply use
|
|
||||||
:tl:`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 :tl:`GetParticipantsRequest` returns :tl:`ChannelParticipants`,
|
|
||||||
which may have more information you need (like the role of the
|
|
||||||
participants, total count of members, etc.)
|
|
||||||
|
|
||||||
__ https://github.com/LonamiWebs/Telethon/issues/573
|
|
||||||
|
|
||||||
|
|
||||||
Recent Actions
|
|
||||||
**************
|
|
||||||
|
|
||||||
"Recent actions" is simply the name official applications have given to
|
|
||||||
the "admin log". Simply use :tl:`GetAdminLogRequest` for that, and
|
|
||||||
you'll get AdminLogResults.events in return which in turn has the final
|
|
||||||
`.action`__.
|
|
||||||
|
|
||||||
__ https://lonamiwebs.github.io/Telethon/types/channel_admin_log_event_action.html
|
|
||||||
|
|
||||||
|
|
||||||
Admin Permissions
|
|
||||||
*****************
|
|
||||||
|
|
||||||
Giving or revoking admin permissions can be done with the :tl:`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.
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
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`__).
|
|
||||||
|
|
||||||
|
|
||||||
Restricting Users
|
|
||||||
*****************
|
|
||||||
|
|
||||||
Similar to how you give or revoke admin permissions, you can edit the
|
|
||||||
banned rights of an user through :tl:`EditBannedRequest` and its parameter
|
|
||||||
:tl:`ChannelBannedRights`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.channels import EditBannedRequest
|
|
||||||
from telethon.tl.types import ChannelBannedRights
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
# Restricting an user for 7 days, only allowing view/send messages.
|
|
||||||
#
|
|
||||||
# Note that it's "reversed". You must set to ``True`` the permissions
|
|
||||||
# you want to REMOVE, and leave as ``None`` those you want to KEEP.
|
|
||||||
rights = ChannelBannedRights(
|
|
||||||
until_date=datetime.now() + timedelta(days=7),
|
|
||||||
view_messages=None,
|
|
||||||
send_messages=None,
|
|
||||||
send_media=True,
|
|
||||||
send_stickers=True,
|
|
||||||
send_gifs=True,
|
|
||||||
send_games=True,
|
|
||||||
send_inline=True,
|
|
||||||
embed_links=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# The above is equivalent to
|
|
||||||
rights = ChannelBannedRights(
|
|
||||||
until_date=datetime.now() + timedelta(days=7),
|
|
||||||
send_media=True,
|
|
||||||
send_stickers=True,
|
|
||||||
send_gifs=True,
|
|
||||||
send_games=True,
|
|
||||||
send_inline=True,
|
|
||||||
embed_links=True
|
|
||||||
)
|
|
||||||
|
|
||||||
client(EditBannedRequest(channel, user, rights))
|
|
||||||
|
|
||||||
|
|
||||||
Kicking a member
|
|
||||||
****************
|
|
||||||
|
|
||||||
Telegram doesn't actually have a request to kick an user from a group.
|
|
||||||
Instead, you need to restrict them so they can't see messages. Any date
|
|
||||||
is enough:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.channels import EditBannedRequest
|
|
||||||
from telethon.tl.types import ChannelBannedRights
|
|
||||||
|
|
||||||
client(EditBannedRequest(
|
|
||||||
channel, user, ChannelBannedRights(
|
|
||||||
until_date=None,
|
|
||||||
view_messages=True
|
|
||||||
)
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
__ 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 :tl:`GetMessagesViewsRequest`, setting ``increment=True``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
|
|
||||||
# Obtain `channel' through dialogs or through client.get_entity() or anyhow.
|
|
||||||
# Obtain `msg_ids' through `.get_messages()` 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
|
|
|
@ -1,44 +0,0 @@
|
||||||
=======================
|
|
||||||
Projects using Telethon
|
|
||||||
=======================
|
|
||||||
|
|
||||||
This page lists some real world examples showcasing what can be built with
|
|
||||||
the library.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Do you have a project that uses the library or know of any that's not
|
|
||||||
listed here? Feel free to leave a comment at
|
|
||||||
`issue 744 <https://github.com/LonamiWebs/Telethon/issues/744>`_
|
|
||||||
so it can be included in the next revision of the documentation!
|
|
||||||
|
|
||||||
.. _projects-telegram-export:
|
|
||||||
|
|
||||||
telegram-export
|
|
||||||
***************
|
|
||||||
|
|
||||||
`Link <https://github.com/expectocode/telegram-export>`_ /
|
|
||||||
`Author's website <https://github.com/expectocode>`_
|
|
||||||
|
|
||||||
A tool to download Telegram data (users, chats, messages, and media)
|
|
||||||
into a database (and display the saved data).
|
|
||||||
|
|
||||||
.. _projects-mautrix-telegram:
|
|
||||||
|
|
||||||
mautrix-telegram
|
|
||||||
****************
|
|
||||||
|
|
||||||
`Link <https://github.com/tulir/mautrix-telegram>`_ /
|
|
||||||
`Author's website <https://maunium.net/>`_
|
|
||||||
|
|
||||||
A Matrix-Telegram hybrid puppeting/relaybot bridge.
|
|
||||||
|
|
||||||
.. _projects-telegramtui:
|
|
||||||
|
|
||||||
TelegramTUI
|
|
||||||
***********
|
|
||||||
|
|
||||||
`Link <https://github.com/bad-day/TelegramTUI>`_ /
|
|
||||||
`Author's website <https://github.com/bad-day>`_
|
|
||||||
|
|
||||||
A Telegram client on your terminal.
|
|
|
@ -1,72 +0,0 @@
|
||||||
=====
|
|
||||||
Users
|
|
||||||
=====
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
These examples assume you have read :ref:`accessing-the-full-api`.
|
|
||||||
|
|
||||||
|
|
||||||
Retrieving full information
|
|
||||||
***************************
|
|
||||||
|
|
||||||
If you need to retrieve the bio, biography or about information for an user
|
|
||||||
you should use :tl:`GetFullUser`:
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.users import GetFullUserRequest
|
|
||||||
|
|
||||||
full = client(GetFullUserRequest(user))
|
|
||||||
# or even
|
|
||||||
full = client(GetFullUserRequest('username'))
|
|
||||||
|
|
||||||
bio = full.about
|
|
||||||
|
|
||||||
|
|
||||||
See :tl:`UserFull` to know what other fields you can access.
|
|
||||||
|
|
||||||
|
|
||||||
Updating your name and/or bio
|
|
||||||
*****************************
|
|
||||||
|
|
||||||
The first name, last name and bio (about) can all be changed with the same
|
|
||||||
request. Omitted fields won't change after invoking :tl:`UpdateProfile`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.account import UpdateProfileRequest
|
|
||||||
|
|
||||||
client(UpdateProfileRequest(a
|
|
||||||
bout='This is a test from Telethon'
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
Updating your username
|
|
||||||
**********************
|
|
||||||
|
|
||||||
You need to use :tl:`account.UpdateUsername`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.account import UpdateUsernameRequest
|
|
||||||
|
|
||||||
client(UpdateUsernameRequest('new_username'))
|
|
||||||
|
|
||||||
|
|
||||||
Updating your profile photo
|
|
||||||
***************************
|
|
||||||
|
|
||||||
The easiest way is to upload a new file and use that as the profile photo
|
|
||||||
through :tl:`UploadProfilePhoto`:
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.photos import UploadProfilePhotoRequest
|
|
||||||
|
|
||||||
client(UploadProfilePhotoRequest(
|
|
||||||
client.upload_file('/path/to/some/file')
|
|
||||||
)))
|
|
|
@ -1,138 +0,0 @@
|
||||||
=====================
|
|
||||||
Working with messages
|
|
||||||
=====================
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
These examples assume you have read :ref:`accessing-the-full-api`.
|
|
||||||
|
|
||||||
|
|
||||||
Forwarding messages
|
|
||||||
*******************
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Use the `telethon.client.messages.MessageMethods.forward_messages`
|
|
||||||
friendly method instead unless you have a better reason not to!
|
|
||||||
|
|
||||||
This method automatically accepts either a single message or many of them.
|
|
||||||
|
|
||||||
.. 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
|
|
||||||
*******************
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Use the `telethon.client.messages.MessageMethods.iter_messages`
|
|
||||||
friendly method instead unless you have a better reason not to!
|
|
||||||
|
|
||||||
This method has ``search`` and ``filter`` parameters that will
|
|
||||||
suit your needs.
|
|
||||||
|
|
||||||
Messages are searched through the obvious :tl:`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)
|
|
||||||
hash=0 # Special number to return nothing on no-change
|
|
||||||
))
|
|
||||||
|
|
||||||
It's important to note that the optional parameter ``from_id`` could have
|
|
||||||
been omitted (defaulting to ``None``). Changing it to :tl:`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 :tl:`InputUser` or :tl:`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
|
|
||||||
from telethon.tl.functions.messages import GetAllStickersRequest
|
|
||||||
sticker_sets = client(GetAllStickersRequest(0))
|
|
||||||
|
|
||||||
# Choose a sticker set
|
|
||||||
from telethon.tl.functions.messages import GetStickerSetRequest
|
|
||||||
from telethon.tl.types import InputStickerSetID
|
|
||||||
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.send_file('me', stickers.documents[0])
|
|
||||||
|
|
||||||
|
|
||||||
.. _issues: https://github.com/LonamiWebs/Telethon/issues/215
|
|
|
@ -1,27 +0,0 @@
|
||||||
========================================
|
|
||||||
Deleted, Limited or Deactivated Accounts
|
|
||||||
========================================
|
|
||||||
|
|
||||||
If you're from Iran or Russia, we have bad news for you. Telegram is much more
|
|
||||||
likely to ban these numbers, as they are often used to spam other accounts,
|
|
||||||
likely through the use of libraries like this one. The best advice we can
|
|
||||||
give you is to not abuse the API, like calling many requests really quickly,
|
|
||||||
and to sign up with these phones through an official application.
|
|
||||||
|
|
||||||
We have also had reports from Kazakhstan and China, where connecting
|
|
||||||
would fail. To solve these connection problems, you should use a proxy.
|
|
||||||
|
|
||||||
Telegram may also ban virtual (VoIP) phone numbers,
|
|
||||||
as again, they're likely to be used for spam.
|
|
||||||
|
|
||||||
If you want to check if your account has been limited,
|
|
||||||
simply send a private message to `@SpamBot`__ through Telegram itself.
|
|
||||||
You should notice this by getting errors like ``PeerFloodError``,
|
|
||||||
which means you're limited, for instance,
|
|
||||||
when sending a message to some accounts but not others.
|
|
||||||
|
|
||||||
For more discussion, please see `issue 297`__.
|
|
||||||
|
|
||||||
|
|
||||||
__ https://t.me/SpamBot
|
|
||||||
__ https://github.com/LonamiWebs/Telethon/issues/297
|
|
|
@ -1,40 +0,0 @@
|
||||||
================
|
|
||||||
Enabling Logging
|
|
||||||
================
|
|
||||||
|
|
||||||
Telethon makes use of the `logging`__ module, and you can enable it as follows:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
logger.debug('Debug messages')
|
|
||||||
logger.info('Useful information')
|
|
||||||
logger.warning('This is a warning!')
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
|
@ -1,29 +0,0 @@
|
||||||
==========
|
|
||||||
RPC Errors
|
|
||||||
==========
|
|
||||||
|
|
||||||
RPC stands for Remote Procedure Call, and when the library raises
|
|
||||||
a ``RPCError``, it's because you have invoked some of the API
|
|
||||||
methods incorrectly (wrong parameters, wrong permissions, or even
|
|
||||||
something went wrong on Telegram's server). All the errors are
|
|
||||||
available in :ref:`telethon-errors-package`, but some examples are:
|
|
||||||
|
|
||||||
- ``FloodWaitError`` (420), the same request was repeated many times.
|
|
||||||
Must wait ``.seconds`` (you can access this parameter).
|
|
||||||
- ``SessionPasswordNeededError``, if you have setup two-steps
|
|
||||||
verification on Telegram.
|
|
||||||
- ``CdnFileTamperedError``, if the media you were trying to download
|
|
||||||
from a CDN has been altered.
|
|
||||||
- ``ChatAdminRequiredError``, you don't have permissions to perform
|
|
||||||
said operation on a chat or channel. Try avoiding filters, i.e. when
|
|
||||||
searching messages.
|
|
||||||
|
|
||||||
The generic classes for different error codes are:
|
|
||||||
|
|
||||||
- ``InvalidDCError`` (303), the request must be repeated on another DC.
|
|
||||||
- ``BadRequestError`` (400), the request contained errors.
|
|
||||||
- ``UnauthorizedError`` (401), the user is not authorized yet.
|
|
||||||
- ``ForbiddenError`` (403), privacy violation error.
|
|
||||||
- ``NotFoundError`` (404), make sure you're invoking ``Request``\ 's!
|
|
||||||
|
|
||||||
If the error is not recognised, it will only be an ``RPCError``.
|
|
|
@ -1,63 +0,0 @@
|
||||||
=============
|
|
||||||
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
|
|
|
@ -1,135 +0,0 @@
|
||||||
.. 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.
|
|
||||||
|
|
||||||
====================================
|
|
||||||
Welcome to Telethon's documentation!
|
|
||||||
====================================
|
|
||||||
|
|
||||||
|
|
||||||
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-client`.
|
|
||||||
|
|
||||||
The mentioned :ref:`telethon-client` is an important section and it
|
|
||||||
contains the friendly methods that **you should use** most of the time.
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
The library uses `asyncio <https://docs.python.org/3/library/asyncio.html>`_
|
|
||||||
under the hood, but you don't need to know anything about it unless you're
|
|
||||||
going to work with updates! If you're an user of Telethon pre-1.0 and you
|
|
||||||
aren't ready to convert your event handlers into ``async``, you can use
|
|
||||||
`a simpler version <https://github.com/LonamiWebs/Telethon/tree/sync>`_
|
|
||||||
(select the "sync" version in ``readthedocs``' bottom left corner).
|
|
||||||
|
|
||||||
If you used Telethon pre-1.0 but your scripts don't use updates or threads,
|
|
||||||
running ``import telethon.sync`` should make them Just Work. Otherwise,
|
|
||||||
we have :ref:`asyncio-magic` to teach you why ``asyncio`` is good and
|
|
||||||
how to use it.
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
:caption: Installation and Simple Usage
|
|
||||||
|
|
||||||
extra/basic/getting-started
|
|
||||||
extra/basic/installation
|
|
||||||
extra/basic/creating-a-client
|
|
||||||
extra/basic/telegram-client
|
|
||||||
extra/basic/entities
|
|
||||||
extra/basic/asyncio-magic
|
|
||||||
extra/basic/working-with-updates
|
|
||||||
|
|
||||||
|
|
||||||
.. _Advanced-usage:
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
:caption: Advanced Usage
|
|
||||||
|
|
||||||
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/users
|
|
||||||
extra/examples/bots
|
|
||||||
extra/examples/projects-using-telethon
|
|
||||||
|
|
||||||
|
|
||||||
.. _Troubleshooting:
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
:caption: Troubleshooting
|
|
||||||
|
|
||||||
extra/troubleshooting/enable-logging
|
|
||||||
extra/troubleshooting/deleted-limited-or-deactivated-accounts
|
|
||||||
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
|
|
||||||
|
|
||||||
modules
|
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
|
@ -1,36 +0,0 @@
|
||||||
@ECHO OFF
|
|
||||||
|
|
||||||
pushd %~dp0
|
|
||||||
|
|
||||||
REM Command file for Sphinx documentation
|
|
||||||
|
|
||||||
if "%SPHINXBUILD%" == "" (
|
|
||||||
set SPHINXBUILD=sphinx-build
|
|
||||||
)
|
|
||||||
set SOURCEDIR=.
|
|
||||||
set BUILDDIR=_build
|
|
||||||
set SPHINXPROJ=Telethon
|
|
||||||
|
|
||||||
if "%1" == "" goto help
|
|
||||||
|
|
||||||
%SPHINXBUILD% >NUL 2>NUL
|
|
||||||
if errorlevel 9009 (
|
|
||||||
echo.
|
|
||||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
|
||||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
|
||||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
|
||||||
echo.may add the Sphinx directory to PATH.
|
|
||||||
echo.
|
|
||||||
echo.If you don't have Sphinx installed, grab it from
|
|
||||||
echo.http://sphinx-doc.org/
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
|
||||||
goto end
|
|
||||||
|
|
||||||
:help
|
|
||||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
|
||||||
|
|
||||||
:end
|
|
||||||
popd
|
|
|
@ -1,7 +0,0 @@
|
||||||
telethon
|
|
||||||
========
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 3
|
|
||||||
|
|
||||||
telethon
|
|
|
@ -1 +0,0 @@
|
||||||
telethon
|
|
|
@ -1,81 +0,0 @@
|
||||||
.. _telethon-client:
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.client package
|
|
||||||
========================
|
|
||||||
|
|
||||||
The `telethon.TelegramClient` aggregates several mixin classes to provide
|
|
||||||
all the common functionality in a nice, Pythonic interface. Each mixin has
|
|
||||||
its own methods, which you all can use.
|
|
||||||
|
|
||||||
**In short, to create a client you must run:**
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from telethon import TelegramClient
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
client = await TelegramClient(name, api_id, api_hash).start()
|
|
||||||
# Now you can use all client methods listed below, like for example...
|
|
||||||
await client.send_message('me', 'Hello to myself!')
|
|
||||||
|
|
||||||
asyncio.get_event_loop().run_until_complete(main())
|
|
||||||
|
|
||||||
|
|
||||||
You **don't** need to import these `AuthMethods`, `MessageMethods`, etc.
|
|
||||||
Together they are the `telethon.TelegramClient` and you can access all of
|
|
||||||
their methods.
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.client.auth
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.client.chats
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: telethon.client.dialogs
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: telethon.client.messageparse
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: telethon.client.messages
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: telethon.client.updates
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: telethon.client.downloads
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: telethon.client.uploads
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: telethon.client.users
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.client.telegrambaseclient
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,22 +0,0 @@
|
||||||
.. _telethon-errors-package:
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.errors package
|
|
||||||
========================
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.errors\.common module
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.errors.common
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.errors\.rpcbaseerrors module
|
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.errors.rpcbaseerrors
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,60 +0,0 @@
|
||||||
.. _telethon-events-package:
|
|
||||||
|
|
||||||
telethon\.events package
|
|
||||||
========================
|
|
||||||
|
|
||||||
Every event (builder) subclasses `telethon.events.common.EventBuilder`,
|
|
||||||
so all the methods in it can be used from any event builder/event instance.
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.common
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.newmessage
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.chataction
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.userupdate
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.messageedited
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.messagedeleted
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.messageread
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.raw
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,35 +0,0 @@
|
||||||
telethon\.extensions package
|
|
||||||
============================
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.extensions\.binaryreader module
|
|
||||||
-----------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.extensions.binaryreader
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.extensions\.markdown module
|
|
||||||
-------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.extensions.markdown
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.extensions\.html module
|
|
||||||
---------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.extensions.html
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.extensions\.tcpclient module
|
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.extensions.tcpclient
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,35 +0,0 @@
|
||||||
telethon\.network package
|
|
||||||
=========================
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.network\.connection module
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.network.connection
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.network\.mtprotoplainsender module
|
|
||||||
------------------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.network.mtprotoplainsender
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.network\.mtprotosender module
|
|
||||||
-----------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.network.mtprotosender
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.network\.authenticator module
|
|
||||||
---------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.network.authenticator
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,81 +0,0 @@
|
||||||
.. _telethon-package:
|
|
||||||
|
|
||||||
|
|
||||||
telethon package
|
|
||||||
================
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.client module
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.client
|
|
||||||
|
|
||||||
.. automodule:: telethon.client
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.utils module
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.utils
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.events package
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.events
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.sessions module
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.sessions
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.errors package
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.errors
|
|
||||||
|
|
||||||
telethon\.extensions package
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.extensions
|
|
||||||
|
|
||||||
telethon\.network package
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.network
|
|
||||||
|
|
||||||
telethon\.tl package
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.tl
|
|
||||||
|
|
||||||
|
|
||||||
Module contents
|
|
||||||
---------------
|
|
||||||
|
|
||||||
.. automodule:: telethon
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,47 +0,0 @@
|
||||||
telethon\.tl\.custom package
|
|
||||||
============================
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.draft module
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.draft
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.dialog module
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.dialog
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.message module
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.message
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.messagebutton module
|
|
||||||
------------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.messagebutton
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.forward module
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.forward
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,16 +0,0 @@
|
||||||
telethon\.tl\.custom package
|
|
||||||
============================
|
|
||||||
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.tl.custom
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.tl\.tlobject module
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.tlobject
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
5
setup.py
5
setup.py
|
@ -183,7 +183,7 @@ def main():
|
||||||
version = re.search(r"^__version__\s*=\s*'(.*)'.*$",
|
version = re.search(r"^__version__\s*=\s*'(.*)'.*$",
|
||||||
f.read(), flags=re.MULTILINE).group(1)
|
f.read(), flags=re.MULTILINE).group(1)
|
||||||
setup(
|
setup(
|
||||||
name='Telethon',
|
name='Telethon-sync',
|
||||||
version=version,
|
version=version,
|
||||||
description="Full-featured Telegram client library for Python 3",
|
description="Full-featured Telegram client library for Python 3",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
|
@ -199,7 +199,7 @@ def main():
|
||||||
# See https://stackoverflow.com/a/40300957/4759433
|
# See https://stackoverflow.com/a/40300957/4759433
|
||||||
# -> https://www.python.org/dev/peps/pep-0345/#requires-python
|
# -> https://www.python.org/dev/peps/pep-0345/#requires-python
|
||||||
# -> http://setuptools.readthedocs.io/en/latest/setuptools.html
|
# -> http://setuptools.readthedocs.io/en/latest/setuptools.html
|
||||||
python_requires='>=3.5',
|
python_requires='>=3.4',
|
||||||
|
|
||||||
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||||
classifiers=[
|
classifiers=[
|
||||||
|
@ -214,6 +214,7 @@ def main():
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
|
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.4',
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6'
|
'Programming Language :: Python :: 3.6'
|
||||||
],
|
],
|
||||||
|
|
|
@ -38,7 +38,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
(You are now logged in)
|
(You are now logged in)
|
||||||
|
|
||||||
If the event loop is already running, this method returns a
|
If the event loop is already running, this method returns a
|
||||||
coroutine that you should await on your own code; otherwise
|
coroutine that you should on your own code; otherwise
|
||||||
the loop is ran until said coroutine completes.
|
the loop is ran until said coroutine completes.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -109,17 +109,17 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
else self.loop.run_until_complete(coro)
|
else self.loop.run_until_complete(coro)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _start(
|
def _start(
|
||||||
self, phone, password, bot_token, force_sms,
|
self, phone, password, bot_token, force_sms,
|
||||||
code_callback, first_name, last_name, max_attempts):
|
code_callback, first_name, last_name, max_attempts):
|
||||||
if not self.is_connected():
|
if not self.is_connected():
|
||||||
await self.connect()
|
self.connect()
|
||||||
|
|
||||||
if await self.is_user_authorized():
|
if self.is_user_authorized():
|
||||||
return self
|
return self
|
||||||
|
|
||||||
if bot_token:
|
if bot_token:
|
||||||
await self.sign_in(bot_token=bot_token)
|
self.sign_in(bot_token=bot_token)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# Turn the callable into a valid phone number
|
# Turn the callable into a valid phone number
|
||||||
|
@ -130,16 +130,16 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
attempts = 0
|
attempts = 0
|
||||||
two_step_detected = False
|
two_step_detected = False
|
||||||
|
|
||||||
sent_code = await self.send_code_request(phone, force_sms=force_sms)
|
sent_code = self.send_code_request(phone, force_sms=force_sms)
|
||||||
sign_up = not sent_code.phone_registered
|
sign_up = not sent_code.phone_registered
|
||||||
while attempts < max_attempts:
|
while attempts < max_attempts:
|
||||||
try:
|
try:
|
||||||
if sign_up:
|
if sign_up:
|
||||||
me = await self.sign_up(
|
me = self.sign_up(
|
||||||
code_callback(), first_name, last_name)
|
code_callback(), first_name, last_name)
|
||||||
else:
|
else:
|
||||||
# Raises SessionPasswordNeededError if 2FA enabled
|
# Raises SessionPasswordNeededError if 2FA enabled
|
||||||
me = await self.sign_in(phone, code=code_callback())
|
me = self.sign_in(phone, code=code_callback())
|
||||||
break
|
break
|
||||||
except errors.SessionPasswordNeededError:
|
except errors.SessionPasswordNeededError:
|
||||||
two_step_detected = True
|
two_step_detected = True
|
||||||
|
@ -171,7 +171,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
if callable(password):
|
if callable(password):
|
||||||
for _ in range(max_attempts):
|
for _ in range(max_attempts):
|
||||||
try:
|
try:
|
||||||
me = await self.sign_in(
|
me = self.sign_in(
|
||||||
phone=phone, password=password())
|
phone=phone, password=password())
|
||||||
break
|
break
|
||||||
except errors.PasswordHashInvalidError:
|
except errors.PasswordHashInvalidError:
|
||||||
|
@ -180,7 +180,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
else:
|
else:
|
||||||
raise errors.PasswordHashInvalidError()
|
raise errors.PasswordHashInvalidError()
|
||||||
else:
|
else:
|
||||||
me = await self.sign_in(phone=phone, password=password)
|
me = self.sign_in(phone=phone, password=password)
|
||||||
|
|
||||||
# We won't reach here if any step failed (exit by exception)
|
# We won't reach here if any step failed (exit by exception)
|
||||||
signed, name = 'Signed in successfully as', utils.get_display_name(me)
|
signed, name = 'Signed in successfully as', utils.get_display_name(me)
|
||||||
|
@ -193,7 +193,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def sign_in(
|
def sign_in(
|
||||||
self, phone=None, *, code=None, password=None,
|
self, phone=None, *, code=None, password=None,
|
||||||
bot_token=None, phone_code_hash=None):
|
bot_token=None, phone_code_hash=None):
|
||||||
"""
|
"""
|
||||||
|
@ -228,12 +228,12 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
The signed in user, or the information about
|
The signed in user, or the information about
|
||||||
:meth:`send_code_request`.
|
:meth:`send_code_request`.
|
||||||
"""
|
"""
|
||||||
me = await self.get_me()
|
me = self.get_me()
|
||||||
if me:
|
if me:
|
||||||
return me
|
return me
|
||||||
|
|
||||||
if phone and not code and not password:
|
if phone and not code and not password:
|
||||||
return await self.send_code_request(phone)
|
return self.send_code_request(phone)
|
||||||
elif code:
|
elif code:
|
||||||
phone = utils.parse_phone(phone) or self._phone
|
phone = utils.parse_phone(phone) or self._phone
|
||||||
phone_code_hash = \
|
phone_code_hash = \
|
||||||
|
@ -248,16 +248,16 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
|
|
||||||
# May raise PhoneCodeEmptyError, PhoneCodeExpiredError,
|
# May raise PhoneCodeEmptyError, PhoneCodeExpiredError,
|
||||||
# PhoneCodeHashEmptyError or PhoneCodeInvalidError.
|
# PhoneCodeHashEmptyError or PhoneCodeInvalidError.
|
||||||
result = await self(functions.auth.SignInRequest(
|
result = self(functions.auth.SignInRequest(
|
||||||
phone, phone_code_hash, str(code)))
|
phone, phone_code_hash, str(code)))
|
||||||
elif password:
|
elif password:
|
||||||
salt = (await self(
|
salt = (self(
|
||||||
functions.account.GetPasswordRequest())).current_salt
|
functions.account.GetPasswordRequest())).current_salt
|
||||||
result = await self(functions.auth.CheckPasswordRequest(
|
result = self(functions.auth.CheckPasswordRequest(
|
||||||
helpers.get_password_hash(password, salt)
|
helpers.get_password_hash(password, salt)
|
||||||
))
|
))
|
||||||
elif bot_token:
|
elif bot_token:
|
||||||
result = await self(functions.auth.ImportBotAuthorizationRequest(
|
result = self(functions.auth.ImportBotAuthorizationRequest(
|
||||||
flags=0, bot_auth_token=bot_token,
|
flags=0, bot_auth_token=bot_token,
|
||||||
api_id=self.api_id, api_hash=self.api_hash
|
api_id=self.api_id, api_hash=self.api_hash
|
||||||
))
|
))
|
||||||
|
@ -273,7 +273,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
self._authorized = True
|
self._authorized = True
|
||||||
return result.user
|
return result.user
|
||||||
|
|
||||||
async def sign_up(self, code, first_name, last_name=''):
|
def sign_up(self, code, first_name, last_name=''):
|
||||||
"""
|
"""
|
||||||
Signs up to Telegram if you don't have an account yet.
|
Signs up to Telegram if you don't have an account yet.
|
||||||
You must call .send_code_request(phone) first.
|
You must call .send_code_request(phone) first.
|
||||||
|
@ -296,7 +296,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
Returns:
|
Returns:
|
||||||
The new created :tl:`User`.
|
The new created :tl:`User`.
|
||||||
"""
|
"""
|
||||||
me = await self.get_me()
|
me = self.get_me()
|
||||||
if me:
|
if me:
|
||||||
return me
|
return me
|
||||||
|
|
||||||
|
@ -308,7 +308,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
sys.stderr.write("{}\n".format(t))
|
sys.stderr.write("{}\n".format(t))
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
result = await self(functions.auth.SignUpRequest(
|
result = self(functions.auth.SignUpRequest(
|
||||||
phone_number=self._phone,
|
phone_number=self._phone,
|
||||||
phone_code_hash=self._phone_code_hash.get(self._phone, ''),
|
phone_code_hash=self._phone_code_hash.get(self._phone, ''),
|
||||||
phone_code=str(code),
|
phone_code=str(code),
|
||||||
|
@ -317,7 +317,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
))
|
))
|
||||||
|
|
||||||
if self._tos:
|
if self._tos:
|
||||||
await self(
|
self(
|
||||||
functions.help.AcceptTermsOfServiceRequest(self._tos.id))
|
functions.help.AcceptTermsOfServiceRequest(self._tos.id))
|
||||||
|
|
||||||
self._self_input_peer = utils.get_input_peer(
|
self._self_input_peer = utils.get_input_peer(
|
||||||
|
@ -326,7 +326,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
self._authorized = True
|
self._authorized = True
|
||||||
return result.user
|
return result.user
|
||||||
|
|
||||||
async def send_code_request(self, phone, *, force_sms=False):
|
def send_code_request(self, phone, *, force_sms=False):
|
||||||
"""
|
"""
|
||||||
Sends a code request to the specified phone number.
|
Sends a code request to the specified phone number.
|
||||||
|
|
||||||
|
@ -345,7 +345,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
|
|
||||||
if not phone_hash:
|
if not phone_hash:
|
||||||
try:
|
try:
|
||||||
result = await self(functions.auth.SendCodeRequest(
|
result = self(functions.auth.SendCodeRequest(
|
||||||
phone, self.api_id, self.api_hash))
|
phone, self.api_id, self.api_hash))
|
||||||
except errors.AuthRestartError:
|
except errors.AuthRestartError:
|
||||||
return self.send_code_request(phone, force_sms=force_sms)
|
return self.send_code_request(phone, force_sms=force_sms)
|
||||||
|
@ -358,14 +358,14 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
self._phone = phone
|
self._phone = phone
|
||||||
|
|
||||||
if force_sms:
|
if force_sms:
|
||||||
result = await self(
|
result = self(
|
||||||
functions.auth.ResendCodeRequest(phone, phone_hash))
|
functions.auth.ResendCodeRequest(phone, phone_hash))
|
||||||
|
|
||||||
self._phone_code_hash[phone] = result.phone_code_hash
|
self._phone_code_hash[phone] = result.phone_code_hash
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def log_out(self):
|
def log_out(self):
|
||||||
"""
|
"""
|
||||||
Logs out Telegram and deletes the current ``*.session`` file.
|
Logs out Telegram and deletes the current ``*.session`` file.
|
||||||
|
|
||||||
|
@ -373,16 +373,16 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
``True`` if the operation was successful.
|
``True`` if the operation was successful.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await self(functions.auth.LogOutRequest())
|
self(functions.auth.LogOutRequest())
|
||||||
except errors.RPCError:
|
except errors.RPCError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
await self.disconnect()
|
self.disconnect()
|
||||||
self.session.delete()
|
self.session.delete()
|
||||||
self._authorized = False
|
self._authorized = False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def edit_2fa(
|
def edit_2fa(
|
||||||
self, current_password=None, new_password=None,
|
self, current_password=None, new_password=None,
|
||||||
*, hint='', email=None):
|
*, hint='', email=None):
|
||||||
"""
|
"""
|
||||||
|
@ -418,7 +418,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
if new_password is None and current_password is None:
|
if new_password is None and current_password is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
pass_result = await self(functions.account.GetPasswordRequest())
|
pass_result = self(functions.account.GetPasswordRequest())
|
||||||
if isinstance(
|
if isinstance(
|
||||||
pass_result, types.account.NoPassword) and current_password:
|
pass_result, types.account.NoPassword) and current_password:
|
||||||
current_password = None
|
current_password = None
|
||||||
|
@ -445,11 +445,11 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
)
|
)
|
||||||
if email: # If enabling 2FA or changing email
|
if email: # If enabling 2FA or changing email
|
||||||
new_settings.email = email # TG counts empty string as None
|
new_settings.email = email # TG counts empty string as None
|
||||||
return await self(functions.account.UpdatePasswordSettingsRequest(
|
return self(functions.account.UpdatePasswordSettingsRequest(
|
||||||
current_password_hash, new_settings=new_settings
|
current_password_hash, new_settings=new_settings
|
||||||
))
|
))
|
||||||
else: # Removing existing password
|
else: # Removing existing password
|
||||||
return await self(functions.account.UpdatePasswordSettingsRequest(
|
return self(functions.account.UpdatePasswordSettingsRequest(
|
||||||
current_password_hash,
|
current_password_hash,
|
||||||
new_settings=types.account.PasswordInputSettings(
|
new_settings=types.account.PasswordInputSettings(
|
||||||
new_salt=bytes(),
|
new_salt=bytes(),
|
||||||
|
@ -465,13 +465,13 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self.start()
|
return self.start()
|
||||||
|
|
||||||
async def __aenter__(self):
|
def __aenter__(self):
|
||||||
return await self.start()
|
return self.start()
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
|
|
||||||
async def __aexit__(self, *args):
|
def __aexit__(self, *args):
|
||||||
await self.disconnect()
|
self.disconnect()
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
|
@ -12,7 +12,7 @@ class ChatMethods(UserMethods):
|
||||||
# region Public methods
|
# region Public methods
|
||||||
|
|
||||||
@async_generator
|
@async_generator
|
||||||
async def iter_participants(
|
def iter_participants(
|
||||||
self, entity, limit=None, *, search='',
|
self, entity, limit=None, *, search='',
|
||||||
filter=None, aggressive=False, _total=None):
|
filter=None, aggressive=False, _total=None):
|
||||||
"""
|
"""
|
||||||
|
@ -61,7 +61,7 @@ class ChatMethods(UserMethods):
|
||||||
else:
|
else:
|
||||||
filter = filter()
|
filter = filter()
|
||||||
|
|
||||||
entity = await self.get_input_entity(entity)
|
entity = self.get_input_entity(entity)
|
||||||
if search and (filter
|
if search and (filter
|
||||||
or not isinstance(entity, types.InputPeerChannel)):
|
or not isinstance(entity, types.InputPeerChannel)):
|
||||||
# We need to 'search' ourselves unless we have a PeerChannel
|
# We need to 'search' ourselves unless we have a PeerChannel
|
||||||
|
@ -77,7 +77,7 @@ class ChatMethods(UserMethods):
|
||||||
limit = float('inf') if limit is None else int(limit)
|
limit = float('inf') if limit is None else int(limit)
|
||||||
if isinstance(entity, types.InputPeerChannel):
|
if isinstance(entity, types.InputPeerChannel):
|
||||||
if _total or (aggressive and not filter):
|
if _total or (aggressive and not filter):
|
||||||
total = (await self(functions.channels.GetFullChannelRequest(
|
total = (self(functions.channels.GetFullChannelRequest(
|
||||||
entity
|
entity
|
||||||
))).full_chat.participants_count
|
))).full_chat.participants_count
|
||||||
if _total:
|
if _total:
|
||||||
|
@ -117,7 +117,7 @@ class ChatMethods(UserMethods):
|
||||||
if requests[0].offset > limit:
|
if requests[0].offset > limit:
|
||||||
break
|
break
|
||||||
|
|
||||||
results = await self(requests)
|
results = self(requests)
|
||||||
for i in reversed(range(len(requests))):
|
for i in reversed(range(len(requests))):
|
||||||
participants = results[i]
|
participants = results[i]
|
||||||
if not participants.users:
|
if not participants.users:
|
||||||
|
@ -133,12 +133,12 @@ class ChatMethods(UserMethods):
|
||||||
seen.add(participant.user_id)
|
seen.add(participant.user_id)
|
||||||
user = users[participant.user_id]
|
user = users[participant.user_id]
|
||||||
user.participant = participant
|
user.participant = participant
|
||||||
await yield_(user)
|
yield_(user)
|
||||||
if len(seen) >= limit:
|
if len(seen) >= limit:
|
||||||
return
|
return
|
||||||
|
|
||||||
elif isinstance(entity, types.InputPeerChat):
|
elif isinstance(entity, types.InputPeerChat):
|
||||||
full = await self(
|
full = self(
|
||||||
functions.messages.GetFullChatRequest(entity.chat_id))
|
functions.messages.GetFullChatRequest(entity.chat_id))
|
||||||
if not isinstance(
|
if not isinstance(
|
||||||
full.full_chat.participants, types.ChatParticipants):
|
full.full_chat.participants, types.ChatParticipants):
|
||||||
|
@ -161,17 +161,17 @@ class ChatMethods(UserMethods):
|
||||||
else:
|
else:
|
||||||
user = users[participant.user_id]
|
user = users[participant.user_id]
|
||||||
user.participant = participant
|
user.participant = participant
|
||||||
await yield_(user)
|
yield_(user)
|
||||||
else:
|
else:
|
||||||
if _total:
|
if _total:
|
||||||
_total[0] = 1
|
_total[0] = 1
|
||||||
if limit != 0:
|
if limit != 0:
|
||||||
user = await self.get_entity(entity)
|
user = self.get_entity(entity)
|
||||||
if filter_entity(user):
|
if filter_entity(user):
|
||||||
user.participant = None
|
user.participant = None
|
||||||
await yield_(user)
|
yield_(user)
|
||||||
|
|
||||||
async def get_participants(self, *args, **kwargs):
|
def get_participants(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Same as :meth:`iter_participants`, but returns a list instead
|
Same as :meth:`iter_participants`, but returns a list instead
|
||||||
with an additional ``.total`` attribute on the list.
|
with an additional ``.total`` attribute on the list.
|
||||||
|
@ -179,7 +179,7 @@ class ChatMethods(UserMethods):
|
||||||
total = [0]
|
total = [0]
|
||||||
kwargs['_total'] = total
|
kwargs['_total'] = total
|
||||||
participants = UserList()
|
participants = UserList()
|
||||||
async for x in self.iter_participants(*args, **kwargs):
|
for x in self.iter_participants(*args, **kwargs):
|
||||||
participants.append(x)
|
participants.append(x)
|
||||||
participants.total = total[0]
|
participants.total = total[0]
|
||||||
return participants
|
return participants
|
||||||
|
|
|
@ -13,7 +13,7 @@ class DialogMethods(UserMethods):
|
||||||
# region Public methods
|
# region Public methods
|
||||||
|
|
||||||
@async_generator
|
@async_generator
|
||||||
async def iter_dialogs(
|
def iter_dialogs(
|
||||||
self, limit=None, *, offset_date=None, offset_id=0,
|
self, limit=None, *, offset_date=None, offset_id=0,
|
||||||
offset_peer=types.InputPeerEmpty(), ignore_migrated=False,
|
offset_peer=types.InputPeerEmpty(), ignore_migrated=False,
|
||||||
_total=None):
|
_total=None):
|
||||||
|
@ -56,7 +56,7 @@ class DialogMethods(UserMethods):
|
||||||
if not _total:
|
if not _total:
|
||||||
return
|
return
|
||||||
# Special case, get a single dialog and determine count
|
# Special case, get a single dialog and determine count
|
||||||
dialogs = await self(functions.messages.GetDialogsRequest(
|
dialogs = self(functions.messages.GetDialogsRequest(
|
||||||
offset_date=offset_date,
|
offset_date=offset_date,
|
||||||
offset_id=offset_id,
|
offset_id=offset_id,
|
||||||
offset_peer=offset_peer,
|
offset_peer=offset_peer,
|
||||||
|
@ -74,7 +74,7 @@ class DialogMethods(UserMethods):
|
||||||
)
|
)
|
||||||
while len(seen) < limit:
|
while len(seen) < limit:
|
||||||
req.limit = min(limit - len(seen), 100)
|
req.limit = min(limit - len(seen), 100)
|
||||||
r = await self(req)
|
r = self(req)
|
||||||
|
|
||||||
if _total:
|
if _total:
|
||||||
_total[0] = getattr(r, 'count', len(r.dialogs))
|
_total[0] = getattr(r, 'count', len(r.dialogs))
|
||||||
|
@ -97,7 +97,7 @@ class DialogMethods(UserMethods):
|
||||||
|
|
||||||
if not ignore_migrated or getattr(
|
if not ignore_migrated or getattr(
|
||||||
cd.entity, 'migrated_to', None) is None:
|
cd.entity, 'migrated_to', None) is None:
|
||||||
await yield_(cd)
|
yield_(cd)
|
||||||
|
|
||||||
if len(r.dialogs) < req.limit\
|
if len(r.dialogs) < req.limit\
|
||||||
or not isinstance(r, types.messages.DialogsSlice):
|
or not isinstance(r, types.messages.DialogsSlice):
|
||||||
|
@ -116,7 +116,7 @@ class DialogMethods(UserMethods):
|
||||||
req.offset_id = r.messages[-1].id
|
req.offset_id = r.messages[-1].id
|
||||||
req.exclude_pinned = True
|
req.exclude_pinned = True
|
||||||
|
|
||||||
async def get_dialogs(self, *args, **kwargs):
|
def get_dialogs(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Same as :meth:`iter_dialogs`, but returns a list instead
|
Same as :meth:`iter_dialogs`, but returns a list instead
|
||||||
with an additional ``.total`` attribute on the list.
|
with an additional ``.total`` attribute on the list.
|
||||||
|
@ -124,13 +124,13 @@ class DialogMethods(UserMethods):
|
||||||
total = [0]
|
total = [0]
|
||||||
kwargs['_total'] = total
|
kwargs['_total'] = total
|
||||||
dialogs = UserList()
|
dialogs = UserList()
|
||||||
async for x in self.iter_dialogs(*args, **kwargs):
|
for x in self.iter_dialogs(*args, **kwargs):
|
||||||
dialogs.append(x)
|
dialogs.append(x)
|
||||||
dialogs.total = total[0]
|
dialogs.total = total[0]
|
||||||
return dialogs
|
return dialogs
|
||||||
|
|
||||||
@async_generator
|
@async_generator
|
||||||
async def iter_drafts(self):
|
def iter_drafts(self):
|
||||||
"""
|
"""
|
||||||
Iterator over all open draft messages.
|
Iterator over all open draft messages.
|
||||||
|
|
||||||
|
@ -139,16 +139,16 @@ class DialogMethods(UserMethods):
|
||||||
to change the message or `telethon.tl.custom.draft.Draft.delete`
|
to change the message or `telethon.tl.custom.draft.Draft.delete`
|
||||||
among other things.
|
among other things.
|
||||||
"""
|
"""
|
||||||
r = await self(functions.messages.GetAllDraftsRequest())
|
r = self(functions.messages.GetAllDraftsRequest())
|
||||||
for update in r.updates:
|
for update in r.updates:
|
||||||
await yield_(custom.Draft._from_update(self, update))
|
yield_(custom.Draft._from_update(self, update))
|
||||||
|
|
||||||
async def get_drafts(self):
|
def get_drafts(self):
|
||||||
"""
|
"""
|
||||||
Same as :meth:`iter_drafts`, but returns a list instead.
|
Same as :meth:`iter_drafts`, but returns a list instead.
|
||||||
"""
|
"""
|
||||||
result = []
|
result = []
|
||||||
async for x in self.iter_drafts():
|
for x in self.iter_drafts():
|
||||||
result.append(x)
|
result.append(x)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ class DownloadMethods(UserMethods):
|
||||||
|
|
||||||
# region Public methods
|
# region Public methods
|
||||||
|
|
||||||
async def download_profile_photo(
|
def download_profile_photo(
|
||||||
self, entity, file=None, *, download_big=True):
|
self, entity, file=None, *, download_big=True):
|
||||||
"""
|
"""
|
||||||
Downloads the profile photo of the given entity (user/chat/channel).
|
Downloads the profile photo of the given entity (user/chat/channel).
|
||||||
|
@ -41,7 +41,7 @@ class DownloadMethods(UserMethods):
|
||||||
# ('InputPeer', 'InputUser', 'InputChannel')
|
# ('InputPeer', 'InputUser', 'InputChannel')
|
||||||
INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd)
|
INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd)
|
||||||
if not isinstance(entity, TLObject) or entity.SUBCLASS_OF_ID in INPUTS:
|
if not isinstance(entity, TLObject) or entity.SUBCLASS_OF_ID in INPUTS:
|
||||||
entity = await self.get_entity(entity)
|
entity = self.get_entity(entity)
|
||||||
|
|
||||||
possible_names = []
|
possible_names = []
|
||||||
if entity.SUBCLASS_OF_ID not in ENTITIES:
|
if entity.SUBCLASS_OF_ID not in ENTITIES:
|
||||||
|
@ -53,7 +53,7 @@ class DownloadMethods(UserMethods):
|
||||||
if not hasattr(entity, 'chat_photo'):
|
if not hasattr(entity, 'chat_photo'):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await self._download_photo(
|
return self._download_photo(
|
||||||
entity.chat_photo, file, date=None, progress_callback=None)
|
entity.chat_photo, file, date=None, progress_callback=None)
|
||||||
|
|
||||||
for attr in ('username', 'first_name', 'title'):
|
for attr in ('username', 'first_name', 'title'):
|
||||||
|
@ -75,15 +75,15 @@ class DownloadMethods(UserMethods):
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.download_file(loc, file)
|
self.download_file(loc, file)
|
||||||
return file
|
return file
|
||||||
except errors.LocationInvalidError:
|
except errors.LocationInvalidError:
|
||||||
# See issue #500, Android app fails as of v4.6.0 (1155).
|
# See issue #500, Android app fails as of v4.6.0 (1155).
|
||||||
# The fix seems to be using the full channel chat photo.
|
# The fix seems to be using the full channel chat photo.
|
||||||
ie = await self.get_input_entity(entity)
|
ie = self.get_input_entity(entity)
|
||||||
if isinstance(ie, types.InputPeerChannel):
|
if isinstance(ie, types.InputPeerChannel):
|
||||||
full = await self(functions.channels.GetFullChannelRequest(ie))
|
full = self(functions.channels.GetFullChannelRequest(ie))
|
||||||
return await self._download_photo(
|
return self._download_photo(
|
||||||
full.full_chat.chat_photo, file,
|
full.full_chat.chat_photo, file,
|
||||||
date=None, progress_callback=None
|
date=None, progress_callback=None
|
||||||
)
|
)
|
||||||
|
@ -91,7 +91,7 @@ class DownloadMethods(UserMethods):
|
||||||
# Until there's a report for chats, no need to.
|
# Until there's a report for chats, no need to.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def download_media(self, message, file=None,
|
def download_media(self, message, file=None,
|
||||||
*, progress_callback=None):
|
*, progress_callback=None):
|
||||||
"""
|
"""
|
||||||
Downloads the given media, or the media from a specified Message.
|
Downloads the given media, or the media from a specified Message.
|
||||||
|
@ -129,11 +129,11 @@ class DownloadMethods(UserMethods):
|
||||||
|
|
||||||
if isinstance(media, (types.MessageMediaPhoto, types.Photo,
|
if isinstance(media, (types.MessageMediaPhoto, types.Photo,
|
||||||
types.PhotoSize, types.PhotoCachedSize)):
|
types.PhotoSize, types.PhotoCachedSize)):
|
||||||
return await self._download_photo(
|
return self._download_photo(
|
||||||
media, file, date, progress_callback
|
media, file, date, progress_callback
|
||||||
)
|
)
|
||||||
elif isinstance(media, (types.MessageMediaDocument, types.Document)):
|
elif isinstance(media, (types.MessageMediaDocument, types.Document)):
|
||||||
return await self._download_document(
|
return self._download_document(
|
||||||
media, file, date, progress_callback
|
media, file, date, progress_callback
|
||||||
)
|
)
|
||||||
elif isinstance(media, types.MessageMediaContact):
|
elif isinstance(media, types.MessageMediaContact):
|
||||||
|
@ -141,7 +141,7 @@ class DownloadMethods(UserMethods):
|
||||||
media, file
|
media, file
|
||||||
)
|
)
|
||||||
|
|
||||||
async def download_file(
|
def download_file(
|
||||||
self, input_location, file=None, *, part_size_kb=None,
|
self, input_location, file=None, *, part_size_kb=None,
|
||||||
file_size=None, progress_callback=None):
|
file_size=None, progress_callback=None):
|
||||||
"""
|
"""
|
||||||
|
@ -209,7 +209,7 @@ class DownloadMethods(UserMethods):
|
||||||
offset = 0
|
offset = 0
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
result = await sender.send(functions.upload.GetFileRequest(
|
result = sender.send(functions.upload.GetFileRequest(
|
||||||
input_location, offset, part_size
|
input_location, offset, part_size
|
||||||
))
|
))
|
||||||
if isinstance(result, types.upload.FileCdnRedirect):
|
if isinstance(result, types.upload.FileCdnRedirect):
|
||||||
|
@ -217,7 +217,7 @@ class DownloadMethods(UserMethods):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
except errors.FileMigrateError as e:
|
except errors.FileMigrateError as e:
|
||||||
__log__.info('File lives in another DC')
|
__log__.info('File lives in another DC')
|
||||||
sender = await self._get_exported_sender(e.new_dc)
|
sender = self._get_exported_sender(e.new_dc)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
offset += part_size
|
offset += part_size
|
||||||
|
@ -234,7 +234,7 @@ class DownloadMethods(UserMethods):
|
||||||
progress_callback(f.tell(), file_size)
|
progress_callback(f.tell(), file_size)
|
||||||
finally:
|
finally:
|
||||||
if sender != self._sender:
|
if sender != self._sender:
|
||||||
await sender.disconnect()
|
sender.disconnect()
|
||||||
if isinstance(file, str) or in_memory:
|
if isinstance(file, str) or in_memory:
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
|
@ -242,7 +242,7 @@ class DownloadMethods(UserMethods):
|
||||||
|
|
||||||
# region Private methods
|
# region Private methods
|
||||||
|
|
||||||
async def _download_photo(self, photo, file, date, progress_callback):
|
def _download_photo(self, photo, file, date, progress_callback):
|
||||||
"""Specialized version of .download_media() for photos"""
|
"""Specialized version of .download_media() for photos"""
|
||||||
# Determine the photo and its largest size
|
# Determine the photo and its largest size
|
||||||
if isinstance(photo, types.MessageMediaPhoto):
|
if isinstance(photo, types.MessageMediaPhoto):
|
||||||
|
@ -272,12 +272,12 @@ class DownloadMethods(UserMethods):
|
||||||
f.close()
|
f.close()
|
||||||
return file
|
return file
|
||||||
|
|
||||||
await self.download_file(
|
self.download_file(
|
||||||
photo.location, file, file_size=photo.size,
|
photo.location, file, file_size=photo.size,
|
||||||
progress_callback=progress_callback)
|
progress_callback=progress_callback)
|
||||||
return file
|
return file
|
||||||
|
|
||||||
async def _download_document(
|
def _download_document(
|
||||||
self, document, file, date, progress_callback):
|
self, document, file, date, progress_callback):
|
||||||
"""Specialized version of .download_media() for documents."""
|
"""Specialized version of .download_media() for documents."""
|
||||||
if isinstance(document, types.MessageMediaDocument):
|
if isinstance(document, types.MessageMediaDocument):
|
||||||
|
@ -311,7 +311,7 @@ class DownloadMethods(UserMethods):
|
||||||
date=date, possible_names=possible_names
|
date=date, possible_names=possible_names
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.download_file(
|
self.download_file(
|
||||||
document, file, file_size=file_size,
|
document, file, file_size=file_size,
|
||||||
progress_callback=progress_callback)
|
progress_callback=progress_callback)
|
||||||
return file
|
return file
|
||||||
|
|
|
@ -45,7 +45,7 @@ class MessageParseMethods(UserMethods):
|
||||||
|
|
||||||
# region Private methods
|
# region Private methods
|
||||||
|
|
||||||
async def _replace_with_mention(self, entities, i, user):
|
def _replace_with_mention(self, entities, i, user):
|
||||||
"""
|
"""
|
||||||
Helper method to replace ``entities[i]`` to mention ``user``,
|
Helper method to replace ``entities[i]`` to mention ``user``,
|
||||||
or do nothing if it can't be found.
|
or do nothing if it can't be found.
|
||||||
|
@ -53,12 +53,12 @@ class MessageParseMethods(UserMethods):
|
||||||
try:
|
try:
|
||||||
entities[i] = types.InputMessageEntityMentionName(
|
entities[i] = types.InputMessageEntityMentionName(
|
||||||
entities[i].offset, entities[i].length,
|
entities[i].offset, entities[i].length,
|
||||||
await self.get_input_entity(user)
|
self.get_input_entity(user)
|
||||||
)
|
)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def _parse_message_text(self, message, parse_mode):
|
def _parse_message_text(self, message, parse_mode):
|
||||||
"""
|
"""
|
||||||
Returns a (parsed message, entities) tuple depending on ``parse_mode``.
|
Returns a (parsed message, entities) tuple depending on ``parse_mode``.
|
||||||
"""
|
"""
|
||||||
|
@ -76,10 +76,10 @@ class MessageParseMethods(UserMethods):
|
||||||
m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url)
|
m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url)
|
||||||
if m:
|
if m:
|
||||||
user = int(m.group(1)) if m.group(1) else e.url
|
user = int(m.group(1)) if m.group(1) else e.url
|
||||||
await self._replace_with_mention(msg_entities, i, user)
|
self._replace_with_mention(msg_entities, i, user)
|
||||||
elif isinstance(e, (types.MessageEntityMentionName,
|
elif isinstance(e, (types.MessageEntityMentionName,
|
||||||
types.InputMessageEntityMentionName)):
|
types.InputMessageEntityMentionName)):
|
||||||
await self._replace_with_mention(msg_entities, i, e.user_id)
|
self._replace_with_mention(msg_entities, i, e.user_id)
|
||||||
|
|
||||||
return message, msg_entities
|
return message, msg_entities
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
# region Message retrieval
|
# region Message retrieval
|
||||||
|
|
||||||
@async_generator
|
@async_generator
|
||||||
async def iter_messages(
|
def iter_messages(
|
||||||
self, entity, limit=None, *, offset_date=None, offset_id=0,
|
self, entity, limit=None, *, offset_date=None, offset_id=0,
|
||||||
max_id=0, min_id=0, add_offset=0, search=None, filter=None,
|
max_id=0, min_id=0, add_offset=0, search=None, filter=None,
|
||||||
from_user=None, batch_size=100, wait_time=None, ids=None,
|
from_user=None, batch_size=100, wait_time=None, ids=None,
|
||||||
|
@ -110,13 +110,13 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
# It's possible to get messages by ID without their entity, so only
|
# It's possible to get messages by ID without their entity, so only
|
||||||
# fetch the input version if we're not using IDs or if it was given.
|
# fetch the input version if we're not using IDs or if it was given.
|
||||||
if not ids or entity:
|
if not ids or entity:
|
||||||
entity = await self.get_input_entity(entity)
|
entity = self.get_input_entity(entity)
|
||||||
|
|
||||||
if ids:
|
if ids:
|
||||||
if not utils.is_list_like(ids):
|
if not utils.is_list_like(ids):
|
||||||
ids = (ids,)
|
ids = (ids,)
|
||||||
async for x in self._iter_ids(entity, ids, total=_total):
|
for x in self._iter_ids(entity, ids, total=_total):
|
||||||
await yield_(x)
|
yield_(x)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Telegram doesn't like min_id/max_id. If these IDs are low enough
|
# Telegram doesn't like min_id/max_id. If these IDs are low enough
|
||||||
|
@ -147,7 +147,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
min_id=0,
|
min_id=0,
|
||||||
hash=0,
|
hash=0,
|
||||||
from_id=(
|
from_id=(
|
||||||
await self.get_input_entity(from_user)
|
self.get_input_entity(from_user)
|
||||||
if from_user else None
|
if from_user else None
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -155,7 +155,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
# Telegram completely ignores `from_id` in private
|
# Telegram completely ignores `from_id` in private
|
||||||
# chats, so we need to do this check client-side.
|
# chats, so we need to do this check client-side.
|
||||||
if isinstance(request.from_id, types.InputPeerSelf):
|
if isinstance(request.from_id, types.InputPeerSelf):
|
||||||
from_id = (await self.get_me(input_peer=True)).user_id
|
from_id = (self.get_me(input_peer=True)).user_id
|
||||||
else:
|
else:
|
||||||
from_id = request.from_id
|
from_id = request.from_id
|
||||||
else:
|
else:
|
||||||
|
@ -174,7 +174,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
if not _total:
|
if not _total:
|
||||||
return
|
return
|
||||||
# No messages, but we still need to know the total message count
|
# No messages, but we still need to know the total message count
|
||||||
result = await self(request)
|
result = self(request)
|
||||||
if isinstance(result, types.messages.MessagesNotModified):
|
if isinstance(result, types.messages.MessagesNotModified):
|
||||||
_total[0] = result.count
|
_total[0] = result.count
|
||||||
else:
|
else:
|
||||||
|
@ -191,7 +191,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
start = asyncio.get_event_loop().time()
|
start = asyncio.get_event_loop().time()
|
||||||
# Telegram has a hard limit of 100
|
# Telegram has a hard limit of 100
|
||||||
request.limit = min(limit - have, batch_size)
|
request.limit = min(limit - have, batch_size)
|
||||||
r = await self(request)
|
r = self(request)
|
||||||
if _total:
|
if _total:
|
||||||
_total[0] = getattr(r, 'count', len(r.messages))
|
_total[0] = getattr(r, 'count', len(r.messages))
|
||||||
|
|
||||||
|
@ -213,7 +213,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
# IDs are returned in descending order.
|
# IDs are returned in descending order.
|
||||||
last_id = message.id
|
last_id = message.id
|
||||||
|
|
||||||
await yield_(custom.Message(self, message, entities, entity))
|
yield_(custom.Message(self, message, entities, entity))
|
||||||
have += 1
|
have += 1
|
||||||
|
|
||||||
if len(r.messages) < request.limit:
|
if len(r.messages) < request.limit:
|
||||||
|
@ -243,10 +243,10 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
request.max_date = last_message.date
|
request.max_date = last_message.date
|
||||||
|
|
||||||
now = asyncio.get_event_loop().time()
|
now = asyncio.get_event_loop().time()
|
||||||
await asyncio.sleep(
|
asyncio.sleep(
|
||||||
max(wait_time - (now - start), 0), loop=self._loop)
|
max(wait_time - (now - start), 0), loop=self._loop)
|
||||||
|
|
||||||
async def get_messages(self, *args, **kwargs):
|
def get_messages(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Same as :meth:`iter_messages`, but returns a list instead
|
Same as :meth:`iter_messages`, but returns a list instead
|
||||||
with an additional ``.total`` attribute on the list.
|
with an additional ``.total`` attribute on the list.
|
||||||
|
@ -272,7 +272,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
kwargs['limit'] = 1
|
kwargs['limit'] = 1
|
||||||
|
|
||||||
msgs = UserList()
|
msgs = UserList()
|
||||||
async for x in self.iter_messages(*args, **kwargs):
|
for x in self.iter_messages(*args, **kwargs):
|
||||||
msgs.append(x)
|
msgs.append(x)
|
||||||
msgs.total = total[0]
|
msgs.total = total[0]
|
||||||
if 'ids' in kwargs and not utils.is_list_like(kwargs['ids']):
|
if 'ids' in kwargs and not utils.is_list_like(kwargs['ids']):
|
||||||
|
@ -284,7 +284,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
|
|
||||||
# region Message sending/editing/deleting
|
# region Message sending/editing/deleting
|
||||||
|
|
||||||
async def send_message(
|
def send_message(
|
||||||
self, entity, message='', *, reply_to=None,
|
self, entity, message='', *, reply_to=None,
|
||||||
parse_mode=utils.Default, link_preview=True, file=None,
|
parse_mode=utils.Default, link_preview=True, file=None,
|
||||||
force_document=False, clear_draft=False):
|
force_document=False, clear_draft=False):
|
||||||
|
@ -340,7 +340,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
The sent `telethon.tl.custom.message.Message`.
|
The sent `telethon.tl.custom.message.Message`.
|
||||||
"""
|
"""
|
||||||
if file is not None:
|
if file is not None:
|
||||||
return await self.send_file(
|
return self.send_file(
|
||||||
entity, file, caption=message, reply_to=reply_to,
|
entity, file, caption=message, reply_to=reply_to,
|
||||||
parse_mode=parse_mode, force_document=force_document
|
parse_mode=parse_mode, force_document=force_document
|
||||||
)
|
)
|
||||||
|
@ -349,11 +349,11 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
'The message cannot be empty unless a file is provided'
|
'The message cannot be empty unless a file is provided'
|
||||||
)
|
)
|
||||||
|
|
||||||
entity = await self.get_input_entity(entity)
|
entity = self.get_input_entity(entity)
|
||||||
if isinstance(message, types.Message):
|
if isinstance(message, types.Message):
|
||||||
if (message.media and not isinstance(
|
if (message.media and not isinstance(
|
||||||
message.media, types.MessageMediaWebPage)):
|
message.media, types.MessageMediaWebPage)):
|
||||||
return await self.send_file(
|
return self.send_file(
|
||||||
entity, message.media, caption=message.message,
|
entity, message.media, caption=message.message,
|
||||||
entities=message.entities
|
entities=message.entities
|
||||||
)
|
)
|
||||||
|
@ -377,7 +377,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
)
|
)
|
||||||
message = message.message
|
message = message.message
|
||||||
else:
|
else:
|
||||||
message, msg_ent = await self._parse_message_text(message,
|
message, msg_ent = self._parse_message_text(message,
|
||||||
parse_mode)
|
parse_mode)
|
||||||
request = functions.messages.SendMessageRequest(
|
request = functions.messages.SendMessageRequest(
|
||||||
peer=entity,
|
peer=entity,
|
||||||
|
@ -388,7 +388,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
clear_draft=clear_draft
|
clear_draft=clear_draft
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await self(request)
|
result = self(request)
|
||||||
if isinstance(result, types.UpdateShortSentMessage):
|
if isinstance(result, types.UpdateShortSentMessage):
|
||||||
to_id, cls = utils.resolve_id(utils.get_peer_id(entity))
|
to_id, cls = utils.resolve_id(utils.get_peer_id(entity))
|
||||||
return custom.Message(self, types.Message(
|
return custom.Message(self, types.Message(
|
||||||
|
@ -403,7 +403,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
|
|
||||||
return self._get_response_message(request, result, entity)
|
return self._get_response_message(request, result, entity)
|
||||||
|
|
||||||
async def forward_messages(self, entity, messages, *, from_peer=None):
|
def forward_messages(self, entity, messages, *, from_peer=None):
|
||||||
"""
|
"""
|
||||||
Forwards the given message(s) to the specified entity.
|
Forwards the given message(s) to the specified entity.
|
||||||
|
|
||||||
|
@ -448,7 +448,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
id=[m if isinstance(m, int) else m.id for m in messages],
|
id=[m if isinstance(m, int) else m.id for m in messages],
|
||||||
to_peer=entity
|
to_peer=entity
|
||||||
)
|
)
|
||||||
result = await self(req)
|
result = self(req)
|
||||||
if isinstance(result, (types.Updates, types.UpdatesCombined)):
|
if isinstance(result, (types.Updates, types.UpdatesCombined)):
|
||||||
entities = {utils.get_peer_id(x): x
|
entities = {utils.get_peer_id(x): x
|
||||||
for x in itertools.chain(result.users, result.chats)}
|
for x in itertools.chain(result.users, result.chats)}
|
||||||
|
@ -468,7 +468,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
result = [id_to_message[random_to_id[rnd]] for rnd in req.random_id]
|
result = [id_to_message[random_to_id[rnd]] for rnd in req.random_id]
|
||||||
return result[0] if single else result
|
return result[0] if single else result
|
||||||
|
|
||||||
async def edit_message(
|
def edit_message(
|
||||||
self, entity, message=None, text=None,
|
self, entity, message=None, text=None,
|
||||||
*, parse_mode=utils.Default, link_preview=True, file=None):
|
*, parse_mode=utils.Default, link_preview=True, file=None):
|
||||||
"""
|
"""
|
||||||
|
@ -527,9 +527,9 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
message = entity
|
message = entity
|
||||||
entity = entity.to_id
|
entity = entity.to_id
|
||||||
|
|
||||||
entity = await self.get_input_entity(entity)
|
entity = self.get_input_entity(entity)
|
||||||
text, msg_entities = await self._parse_message_text(text, parse_mode)
|
text, msg_entities = self._parse_message_text(text, parse_mode)
|
||||||
file_handle, media = await self._file_to_media(file)
|
file_handle, media = self._file_to_media(file)
|
||||||
request = functions.messages.EditMessageRequest(
|
request = functions.messages.EditMessageRequest(
|
||||||
peer=entity,
|
peer=entity,
|
||||||
id=utils.get_message_id(message),
|
id=utils.get_message_id(message),
|
||||||
|
@ -538,11 +538,11 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
entities=msg_entities,
|
entities=msg_entities,
|
||||||
media=media
|
media=media
|
||||||
)
|
)
|
||||||
msg = self._get_response_message(request, await self(request), entity)
|
msg = self._get_response_message(request, self(request), entity)
|
||||||
self._cache_media(msg, file, file_handle)
|
self._cache_media(msg, file, file_handle)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
async def delete_messages(self, entity, message_ids, *, revoke=True):
|
def delete_messages(self, entity, message_ids, *, revoke=True):
|
||||||
"""
|
"""
|
||||||
Deletes a message from a chat, optionally "for everyone".
|
Deletes a message from a chat, optionally "for everyone".
|
||||||
|
|
||||||
|
@ -574,19 +574,19 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
else int(m) for m in message_ids
|
else int(m) for m in message_ids
|
||||||
)
|
)
|
||||||
|
|
||||||
entity = await self.get_input_entity(entity) if entity else None
|
entity = self.get_input_entity(entity) if entity else None
|
||||||
if isinstance(entity, types.InputPeerChannel):
|
if isinstance(entity, types.InputPeerChannel):
|
||||||
return await self([functions.channels.DeleteMessagesRequest(
|
return self([functions.channels.DeleteMessagesRequest(
|
||||||
entity, list(c)) for c in utils.chunks(message_ids)])
|
entity, list(c)) for c in utils.chunks(message_ids)])
|
||||||
else:
|
else:
|
||||||
return await self([functions.messages.DeleteMessagesRequest(
|
return self([functions.messages.DeleteMessagesRequest(
|
||||||
list(c), revoke) for c in utils.chunks(message_ids)])
|
list(c), revoke) for c in utils.chunks(message_ids)])
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Miscellaneous
|
# region Miscellaneous
|
||||||
|
|
||||||
async def send_read_acknowledge(
|
def send_read_acknowledge(
|
||||||
self, entity, message=None, *, max_id=None, clear_mentions=False):
|
self, entity, message=None, *, max_id=None, clear_mentions=False):
|
||||||
"""
|
"""
|
||||||
Sends a "read acknowledge" (i.e., notifying the given peer that we've
|
Sends a "read acknowledge" (i.e., notifying the given peer that we've
|
||||||
|
@ -623,18 +623,18 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Either a message list or a max_id must be provided.')
|
'Either a message list or a max_id must be provided.')
|
||||||
|
|
||||||
entity = await self.get_input_entity(entity)
|
entity = self.get_input_entity(entity)
|
||||||
if clear_mentions:
|
if clear_mentions:
|
||||||
await self(functions.messages.ReadMentionsRequest(entity))
|
self(functions.messages.ReadMentionsRequest(entity))
|
||||||
if max_id is None:
|
if max_id is None:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if max_id is not None:
|
if max_id is not None:
|
||||||
if isinstance(entity, types.InputPeerChannel):
|
if isinstance(entity, types.InputPeerChannel):
|
||||||
return await self(functions.channels.ReadHistoryRequest(
|
return self(functions.channels.ReadHistoryRequest(
|
||||||
entity, max_id=max_id))
|
entity, max_id=max_id))
|
||||||
else:
|
else:
|
||||||
return await self(functions.messages.ReadHistoryRequest(
|
return self(functions.messages.ReadHistoryRequest(
|
||||||
entity, max_id=max_id))
|
entity, max_id=max_id))
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -646,7 +646,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
# region Private methods
|
# region Private methods
|
||||||
|
|
||||||
@async_generator
|
@async_generator
|
||||||
async def _iter_ids(self, entity, ids, total):
|
def _iter_ids(self, entity, ids, total):
|
||||||
"""
|
"""
|
||||||
Special case for `iter_messages` when it should only fetch some IDs.
|
Special case for `iter_messages` when it should only fetch some IDs.
|
||||||
"""
|
"""
|
||||||
|
@ -655,15 +655,15 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
|
|
||||||
from_id = None # By default, no need to validate from_id
|
from_id = None # By default, no need to validate from_id
|
||||||
if isinstance(entity, types.InputPeerChannel):
|
if isinstance(entity, types.InputPeerChannel):
|
||||||
r = await self(functions.channels.GetMessagesRequest(entity, ids))
|
r = self(functions.channels.GetMessagesRequest(entity, ids))
|
||||||
else:
|
else:
|
||||||
r = await self(functions.messages.GetMessagesRequest(ids))
|
r = self(functions.messages.GetMessagesRequest(ids))
|
||||||
if entity:
|
if entity:
|
||||||
from_id = utils.get_peer_id(entity)
|
from_id = utils.get_peer_id(entity)
|
||||||
|
|
||||||
if isinstance(r, types.messages.MessagesNotModified):
|
if isinstance(r, types.messages.MessagesNotModified):
|
||||||
for _ in ids:
|
for _ in ids:
|
||||||
await yield_(None)
|
yield_(None)
|
||||||
return
|
return
|
||||||
|
|
||||||
entities = {utils.get_peer_id(x): x
|
entities = {utils.get_peer_id(x): x
|
||||||
|
@ -678,8 +678,8 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
||||||
for message in r.messages:
|
for message in r.messages:
|
||||||
if isinstance(message, types.MessageEmpty) or (
|
if isinstance(message, types.MessageEmpty) or (
|
||||||
from_id and utils.get_peer_id(message.to_id) != from_id):
|
from_id and utils.get_peer_id(message.to_id) != from_id):
|
||||||
await yield_(None)
|
yield_(None)
|
||||||
else:
|
else:
|
||||||
await yield_(custom.Message(self, message, entities, entity))
|
yield_(custom.Message(self, message, entities, entity))
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
|
@ -269,14 +269,14 @@ class TelegramBaseClient(abc.ABC):
|
||||||
|
|
||||||
# region Connecting
|
# region Connecting
|
||||||
|
|
||||||
async def connect(self):
|
def connect(self):
|
||||||
"""
|
"""
|
||||||
Connects to Telegram.
|
Connects to Telegram.
|
||||||
"""
|
"""
|
||||||
await self._sender.connect(
|
self._sender.connect(
|
||||||
self.session.server_address, self.session.port)
|
self.session.server_address, self.session.port)
|
||||||
|
|
||||||
await self._sender.send(self._init_with(
|
self._sender.send(self._init_with(
|
||||||
functions.help.GetConfigRequest()))
|
functions.help.GetConfigRequest()))
|
||||||
|
|
||||||
self._updates_handle = self._loop.create_task(self._update_loop())
|
self._updates_handle = self._loop.create_task(self._update_loop())
|
||||||
|
@ -287,15 +287,15 @@ class TelegramBaseClient(abc.ABC):
|
||||||
"""
|
"""
|
||||||
return self._sender.is_connected()
|
return self._sender.is_connected()
|
||||||
|
|
||||||
async def disconnect(self):
|
def disconnect(self):
|
||||||
"""
|
"""
|
||||||
Disconnects from Telegram.
|
Disconnects from Telegram.
|
||||||
"""
|
"""
|
||||||
await self._disconnect()
|
self._disconnect()
|
||||||
if getattr(self, 'session', None):
|
if getattr(self, 'session', None):
|
||||||
self.session.close()
|
self.session.close()
|
||||||
|
|
||||||
async def _disconnect(self):
|
def _disconnect(self):
|
||||||
"""
|
"""
|
||||||
Disconnect only, without closing the session. Used in reconnections
|
Disconnect only, without closing the session. Used in reconnections
|
||||||
to different data centers, where we don't want to close the session
|
to different data centers, where we don't want to close the session
|
||||||
|
@ -305,9 +305,9 @@ class TelegramBaseClient(abc.ABC):
|
||||||
# All properties may be ``None`` if `__init__` fails, and this
|
# All properties may be ``None`` if `__init__` fails, and this
|
||||||
# method will be called from `__del__` which would crash then.
|
# method will be called from `__del__` which would crash then.
|
||||||
if getattr(self, '_sender', None):
|
if getattr(self, '_sender', None):
|
||||||
await self._sender.disconnect()
|
self._sender.disconnect()
|
||||||
if getattr(self, '_updates_handle', None):
|
if getattr(self, '_updates_handle', None):
|
||||||
await self._updates_handle
|
self._updates_handle
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
if not self.is_connected() or self.loop.is_closed():
|
if not self.is_connected() or self.loop.is_closed():
|
||||||
|
@ -324,20 +324,20 @@ class TelegramBaseClient(abc.ABC):
|
||||||
else:
|
else:
|
||||||
self._loop.run_until_complete(self.disconnect())
|
self._loop.run_until_complete(self.disconnect())
|
||||||
|
|
||||||
async def _switch_dc(self, new_dc):
|
def _switch_dc(self, new_dc):
|
||||||
"""
|
"""
|
||||||
Permanently switches the current connection to the new data center.
|
Permanently switches the current connection to the new data center.
|
||||||
"""
|
"""
|
||||||
__log__.info('Reconnecting to new data center %s', new_dc)
|
__log__.info('Reconnecting to new data center %s', new_dc)
|
||||||
dc = await self._get_dc(new_dc)
|
dc = self._get_dc(new_dc)
|
||||||
|
|
||||||
self.session.set_dc(dc.id, dc.ip_address, 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
|
# 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.
|
# so it's not valid anymore. Set to None to force recreating it.
|
||||||
self.session.auth_key = self._sender.state.auth_key = None
|
self.session.auth_key = self._sender.state.auth_key = None
|
||||||
self.session.save()
|
self.session.save()
|
||||||
await self._disconnect()
|
self._disconnect()
|
||||||
return await self.connect()
|
return self.connect()
|
||||||
|
|
||||||
def _auth_key_callback(self, auth_key):
|
def _auth_key_callback(self, auth_key):
|
||||||
"""
|
"""
|
||||||
|
@ -352,14 +352,14 @@ class TelegramBaseClient(abc.ABC):
|
||||||
|
|
||||||
# region Working with different connections/Data Centers
|
# region Working with different connections/Data Centers
|
||||||
|
|
||||||
async def _get_dc(self, dc_id, cdn=False):
|
def _get_dc(self, dc_id, cdn=False):
|
||||||
"""Gets the Data Center (DC) associated to 'dc_id'"""
|
"""Gets the Data Center (DC) associated to 'dc_id'"""
|
||||||
cls = self.__class__
|
cls = self.__class__
|
||||||
if not cls._config:
|
if not cls._config:
|
||||||
cls._config = await self(functions.help.GetConfigRequest())
|
cls._config = self(functions.help.GetConfigRequest())
|
||||||
|
|
||||||
if cdn and not self._cdn_config:
|
if cdn and not self._cdn_config:
|
||||||
cls._cdn_config = await self(functions.help.GetCdnConfigRequest())
|
cls._cdn_config = self(functions.help.GetCdnConfigRequest())
|
||||||
for pk in cls._cdn_config.public_keys:
|
for pk in cls._cdn_config.public_keys:
|
||||||
rsa.add_key(pk.public_key)
|
rsa.add_key(pk.public_key)
|
||||||
|
|
||||||
|
@ -369,7 +369,7 @@ class TelegramBaseClient(abc.ABC):
|
||||||
and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn
|
and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _get_exported_sender(self, dc_id):
|
def _get_exported_sender(self, dc_id):
|
||||||
"""
|
"""
|
||||||
Returns a cached `MTProtoSender` for the given `dc_id`, or creates
|
Returns a cached `MTProtoSender` for the given `dc_id`, or creates
|
||||||
a new one if it doesn't exist yet, and imports a freshly exported
|
a new one if it doesn't exist yet, and imports a freshly exported
|
||||||
|
@ -378,32 +378,32 @@ class TelegramBaseClient(abc.ABC):
|
||||||
# Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt
|
# Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt
|
||||||
# for clearly showing how to export the authorization
|
# for clearly showing how to export the authorization
|
||||||
auth = self._exported_auths.get(dc_id)
|
auth = self._exported_auths.get(dc_id)
|
||||||
dc = await self._get_dc(dc_id)
|
dc = self._get_dc(dc_id)
|
||||||
state = MTProtoState(auth)
|
state = MTProtoState(auth)
|
||||||
# Can't reuse self._sender._connection as it has its own seqno.
|
# Can't reuse self._sender._connection as it has its own seqno.
|
||||||
#
|
#
|
||||||
# If one were to do that, Telegram would reset the connection
|
# If one were to do that, Telegram would reset the connection
|
||||||
# with no further clues.
|
# with no further clues.
|
||||||
sender = MTProtoSender(state, self._connection.clone(), self._loop)
|
sender = MTProtoSender(state, self._connection.clone(), self._loop)
|
||||||
await sender.connect(dc.ip_address, dc.port)
|
sender.connect(dc.ip_address, dc.port)
|
||||||
if not auth:
|
if not auth:
|
||||||
__log__.info('Exporting authorization for data center %s', dc)
|
__log__.info('Exporting authorization for data center %s', dc)
|
||||||
auth = await self(functions.auth.ExportAuthorizationRequest(dc_id))
|
auth = self(functions.auth.ExportAuthorizationRequest(dc_id))
|
||||||
req = self._init_with(functions.auth.ImportAuthorizationRequest(
|
req = self._init_with(functions.auth.ImportAuthorizationRequest(
|
||||||
id=auth.id, bytes=auth.bytes
|
id=auth.id, bytes=auth.bytes
|
||||||
))
|
))
|
||||||
await sender.send(req)
|
sender.send(req)
|
||||||
self._exported_auths[dc_id] = sender.state.auth_key
|
self._exported_auths[dc_id] = sender.state.auth_key
|
||||||
|
|
||||||
return sender
|
return sender
|
||||||
|
|
||||||
async def _get_cdn_client(self, cdn_redirect):
|
def _get_cdn_client(self, cdn_redirect):
|
||||||
"""Similar to ._get_exported_client, but for CDNs"""
|
"""Similar to ._get_exported_client, but for CDNs"""
|
||||||
# TODO Implement
|
# TODO Implement
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
session = self._exported_sessions.get(cdn_redirect.dc_id)
|
session = self._exported_sessions.get(cdn_redirect.dc_id)
|
||||||
if not session:
|
if not session:
|
||||||
dc = await self._get_dc(cdn_redirect.dc_id, cdn=True)
|
dc = self._get_dc(cdn_redirect.dc_id, cdn=True)
|
||||||
session = self.session.clone()
|
session = self.session.clone()
|
||||||
session.set_dc(dc.id, dc.ip_address, dc.port)
|
session.set_dc(dc.id, dc.ip_address, dc.port)
|
||||||
self._exported_sessions[cdn_redirect.dc_id] = session
|
self._exported_sessions[cdn_redirect.dc_id] = session
|
||||||
|
@ -458,7 +458,7 @@ class TelegramBaseClient(abc.ABC):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def _handle_auto_reconnect(self):
|
def _handle_auto_reconnect(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
|
@ -16,11 +16,11 @@ class UpdateMethods(UserMethods):
|
||||||
|
|
||||||
# region Public methods
|
# region Public methods
|
||||||
|
|
||||||
async def _run_until_disconnected(self):
|
def _run_until_disconnected(self):
|
||||||
try:
|
try:
|
||||||
await self.disconnected
|
self.disconnected
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
await self.disconnect()
|
self.disconnect()
|
||||||
|
|
||||||
def run_until_disconnected(self):
|
def run_until_disconnected(self):
|
||||||
"""
|
"""
|
||||||
|
@ -30,7 +30,7 @@ class UpdateMethods(UserMethods):
|
||||||
to ``except`` it on your own code.
|
to ``except`` it on your own code.
|
||||||
|
|
||||||
If the loop is already running, this method returns a coroutine
|
If the loop is already running, this method returns a coroutine
|
||||||
that you should await on your own code.
|
that you should on your own code.
|
||||||
"""
|
"""
|
||||||
if self.loop.is_running():
|
if self.loop.is_running():
|
||||||
return self._run_until_disconnected()
|
return self._run_until_disconnected()
|
||||||
|
@ -52,7 +52,7 @@ class UpdateMethods(UserMethods):
|
||||||
>>> client = TelegramClient(...)
|
>>> client = TelegramClient(...)
|
||||||
>>>
|
>>>
|
||||||
>>> @client.on(events.NewMessage)
|
>>> @client.on(events.NewMessage)
|
||||||
... async def handler(event):
|
... def handler(event):
|
||||||
... ...
|
... ...
|
||||||
...
|
...
|
||||||
>>>
|
>>>
|
||||||
|
@ -120,7 +120,7 @@ class UpdateMethods(UserMethods):
|
||||||
"""
|
"""
|
||||||
return [(callback, event) for event, callback in self._event_builders]
|
return [(callback, event) for event, callback in self._event_builders]
|
||||||
|
|
||||||
async def catch_up(self):
|
def catch_up(self):
|
||||||
state = self.session.get_update_state(0)
|
state = self.session.get_update_state(0)
|
||||||
if not state or not state.pts:
|
if not state or not state.pts:
|
||||||
return
|
return
|
||||||
|
@ -128,7 +128,7 @@ class UpdateMethods(UserMethods):
|
||||||
self.session.catching_up = True
|
self.session.catching_up = True
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
d = await self(functions.updates.GetDifferenceRequest(
|
d = self(functions.updates.GetDifferenceRequest(
|
||||||
state.pts, state.date, state.qts))
|
state.pts, state.date, state.qts))
|
||||||
if isinstance(d, types.updates.DifferenceEmpty):
|
if isinstance(d, types.updates.DifferenceEmpty):
|
||||||
state.date = d.date
|
state.date = d.date
|
||||||
|
@ -193,12 +193,12 @@ class UpdateMethods(UserMethods):
|
||||||
|
|
||||||
# TODO make use of need_diff
|
# TODO make use of need_diff
|
||||||
|
|
||||||
async def _update_loop(self):
|
def _update_loop(self):
|
||||||
# Pings' ID don't really need to be secure, just "random"
|
# Pings' ID don't really need to be secure, just "random"
|
||||||
rnd = lambda: random.randrange(-2**63, 2**63)
|
rnd = lambda: random.randrange(-2**63, 2**63)
|
||||||
while self.is_connected():
|
while self.is_connected():
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(
|
asyncio.wait_for(
|
||||||
self.disconnected, timeout=60, loop=self._loop
|
self.disconnected, timeout=60, loop=self._loop
|
||||||
)
|
)
|
||||||
continue # We actually just want to act upon timeout
|
continue # We actually just want to act upon timeout
|
||||||
|
@ -223,22 +223,22 @@ class UpdateMethods(UserMethods):
|
||||||
#
|
#
|
||||||
# TODO Call getDifference instead since it's more relevant
|
# TODO Call getDifference instead since it's more relevant
|
||||||
if time.time() - self._last_request > 30 * 60:
|
if time.time() - self._last_request > 30 * 60:
|
||||||
if not await self.is_user_authorized():
|
if not self.is_user_authorized():
|
||||||
# What can be the user doing for so
|
# What can be the user doing for so
|
||||||
# long without being logged in...?
|
# long without being logged in...?
|
||||||
continue
|
continue
|
||||||
|
|
||||||
await self(functions.updates.GetStateRequest())
|
self(functions.updates.GetStateRequest())
|
||||||
|
|
||||||
async def _dispatch_update(self, update):
|
def _dispatch_update(self, update):
|
||||||
if self._events_pending_resolve:
|
if self._events_pending_resolve:
|
||||||
if self._event_resolve_lock.locked():
|
if self._event_resolve_lock.locked():
|
||||||
async with self._event_resolve_lock:
|
with self._event_resolve_lock:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
async with self._event_resolve_lock:
|
with self._event_resolve_lock:
|
||||||
for event in self._events_pending_resolve:
|
for event in self._events_pending_resolve:
|
||||||
await event.resolve(self)
|
event.resolve(self)
|
||||||
|
|
||||||
self._events_pending_resolve.clear()
|
self._events_pending_resolve.clear()
|
||||||
|
|
||||||
|
@ -252,7 +252,7 @@ class UpdateMethods(UserMethods):
|
||||||
|
|
||||||
event.original_update = update
|
event.original_update = update
|
||||||
try:
|
try:
|
||||||
await callback(event)
|
callback(event)
|
||||||
except events.StopPropagation:
|
except events.StopPropagation:
|
||||||
__log__.debug(
|
__log__.debug(
|
||||||
"Event handler '{}' stopped chain of "
|
"Event handler '{}' stopped chain of "
|
||||||
|
@ -265,12 +265,12 @@ class UpdateMethods(UserMethods):
|
||||||
__log__.exception('Unhandled exception on {}'
|
__log__.exception('Unhandled exception on {}'
|
||||||
.format(callback.__name__))
|
.format(callback.__name__))
|
||||||
|
|
||||||
async def _handle_auto_reconnect(self):
|
def _handle_auto_reconnect(self):
|
||||||
# Upon reconnection, we want to send getState
|
# Upon reconnection, we want to send getState
|
||||||
# for Telegram to keep sending us updates.
|
# for Telegram to keep sending us updates.
|
||||||
try:
|
try:
|
||||||
__log__.info('Asking for the current state after reconnect...')
|
__log__.info('Asking for the current state after reconnect...')
|
||||||
state = await self(functions.updates.GetStateRequest())
|
state = self(functions.updates.GetStateRequest())
|
||||||
__log__.info('Got new state! %s', state)
|
__log__.info('Got new state! %s', state)
|
||||||
except errors.RPCError as e:
|
except errors.RPCError as e:
|
||||||
__log__.info('Failed to get current state: %r', e)
|
__log__.info('Failed to get current state: %r', e)
|
||||||
|
|
|
@ -26,7 +26,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
|
|
||||||
# region Public methods
|
# region Public methods
|
||||||
|
|
||||||
async def send_file(
|
def send_file(
|
||||||
self, entity, file, *, caption='', force_document=False,
|
self, entity, file, *, caption='', force_document=False,
|
||||||
progress_callback=None, reply_to=None, attributes=None,
|
progress_callback=None, reply_to=None, attributes=None,
|
||||||
thumb=None, allow_cache=True, parse_mode=utils.Default,
|
thumb=None, allow_cache=True, parse_mode=utils.Default,
|
||||||
|
@ -122,7 +122,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
while images:
|
while images:
|
||||||
result += await self._send_album(
|
result += self._send_album(
|
||||||
entity, images[:10], caption=caption,
|
entity, images[:10], caption=caption,
|
||||||
progress_callback=progress_callback, reply_to=reply_to,
|
progress_callback=progress_callback, reply_to=reply_to,
|
||||||
parse_mode=parse_mode
|
parse_mode=parse_mode
|
||||||
|
@ -130,7 +130,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
images = images[10:]
|
images = images[10:]
|
||||||
|
|
||||||
for x in documents:
|
for x in documents:
|
||||||
result.append(await self.send_file(
|
result.append(self.send_file(
|
||||||
entity, x, allow_cache=allow_cache,
|
entity, x, allow_cache=allow_cache,
|
||||||
caption=caption, force_document=force_document,
|
caption=caption, force_document=force_document,
|
||||||
progress_callback=progress_callback, reply_to=reply_to,
|
progress_callback=progress_callback, reply_to=reply_to,
|
||||||
|
@ -140,7 +140,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
entity = await self.get_input_entity(entity)
|
entity = self.get_input_entity(entity)
|
||||||
reply_to = utils.get_message_id(reply_to)
|
reply_to = utils.get_message_id(reply_to)
|
||||||
|
|
||||||
# Not document since it's subject to change.
|
# Not document since it's subject to change.
|
||||||
|
@ -149,9 +149,9 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
msg_entities = kwargs['entities']
|
msg_entities = kwargs['entities']
|
||||||
else:
|
else:
|
||||||
caption, msg_entities =\
|
caption, msg_entities =\
|
||||||
await self._parse_message_text(caption, parse_mode)
|
self._parse_message_text(caption, parse_mode)
|
||||||
|
|
||||||
file_handle, media = await self._file_to_media(
|
file_handle, media = self._file_to_media(
|
||||||
file, force_document=force_document,
|
file, force_document=force_document,
|
||||||
progress_callback=progress_callback,
|
progress_callback=progress_callback,
|
||||||
attributes=attributes, allow_cache=allow_cache, thumb=thumb,
|
attributes=attributes, allow_cache=allow_cache, thumb=thumb,
|
||||||
|
@ -162,12 +162,12 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
entity, media, reply_to_msg_id=reply_to, message=caption,
|
entity, media, reply_to_msg_id=reply_to, message=caption,
|
||||||
entities=msg_entities
|
entities=msg_entities
|
||||||
)
|
)
|
||||||
msg = self._get_response_message(request, await self(request), entity)
|
msg = self._get_response_message(request, self(request), entity)
|
||||||
self._cache_media(msg, file, file_handle, force_document=force_document)
|
self._cache_media(msg, file, file_handle, force_document=force_document)
|
||||||
|
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
async def _send_album(self, entity, files, caption='',
|
def _send_album(self, entity, files, caption='',
|
||||||
progress_callback=None, reply_to=None,
|
progress_callback=None, reply_to=None,
|
||||||
parse_mode=utils.Default):
|
parse_mode=utils.Default):
|
||||||
"""Specialized version of .send_file for albums"""
|
"""Specialized version of .send_file for albums"""
|
||||||
|
@ -180,13 +180,13 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
# In theory documents can be sent inside the albums but they appear
|
# In theory documents can be sent inside the albums but they appear
|
||||||
# as different messages (not inside the album), and the logic to set
|
# as different messages (not inside the album), and the logic to set
|
||||||
# the attributes/avoid cache is already written in .send_file().
|
# the attributes/avoid cache is already written in .send_file().
|
||||||
entity = await self.get_input_entity(entity)
|
entity = self.get_input_entity(entity)
|
||||||
if not utils.is_list_like(caption):
|
if not utils.is_list_like(caption):
|
||||||
caption = (caption,)
|
caption = (caption,)
|
||||||
|
|
||||||
captions = []
|
captions = []
|
||||||
for c in reversed(caption): # Pop from the end (so reverse)
|
for c in reversed(caption): # Pop from the end (so reverse)
|
||||||
captions.append(await self._parse_message_text(c or '', parse_mode))
|
captions.append(self._parse_message_text(c or '', parse_mode))
|
||||||
|
|
||||||
reply_to = utils.get_message_id(reply_to)
|
reply_to = utils.get_message_id(reply_to)
|
||||||
|
|
||||||
|
@ -194,9 +194,9 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
media = []
|
media = []
|
||||||
for file in files:
|
for file in files:
|
||||||
# fh will either be InputPhoto or a modified InputFile
|
# fh will either be InputPhoto or a modified InputFile
|
||||||
fh = await self.upload_file(file, use_cache=types.InputPhoto)
|
fh = self.upload_file(file, use_cache=types.InputPhoto)
|
||||||
if not isinstance(fh, types.InputPhoto):
|
if not isinstance(fh, types.InputPhoto):
|
||||||
r = await self(functions.messages.UploadMediaRequest(
|
r = self(functions.messages.UploadMediaRequest(
|
||||||
entity, media=types.InputMediaUploadedPhoto(fh)
|
entity, media=types.InputMediaUploadedPhoto(fh)
|
||||||
))
|
))
|
||||||
input_photo = utils.get_input_photo(r.photo)
|
input_photo = utils.get_input_photo(r.photo)
|
||||||
|
@ -211,7 +211,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
entities=msg_entities))
|
entities=msg_entities))
|
||||||
|
|
||||||
# Now we can construct the multi-media request
|
# Now we can construct the multi-media request
|
||||||
result = await self(functions.messages.SendMultiMediaRequest(
|
result = self(functions.messages.SendMultiMediaRequest(
|
||||||
entity, reply_to_msg_id=reply_to, multi_media=media
|
entity, reply_to_msg_id=reply_to, multi_media=media
|
||||||
))
|
))
|
||||||
return [
|
return [
|
||||||
|
@ -220,7 +220,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
if isinstance(update, types.UpdateMessageID)
|
if isinstance(update, types.UpdateMessageID)
|
||||||
]
|
]
|
||||||
|
|
||||||
async def upload_file(
|
def upload_file(
|
||||||
self, file, *, part_size_kb=None, file_name=None, use_cache=None,
|
self, file, *, part_size_kb=None, file_name=None, use_cache=None,
|
||||||
progress_callback=None):
|
progress_callback=None):
|
||||||
"""
|
"""
|
||||||
|
@ -337,7 +337,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
request = functions.upload.SaveFilePartRequest(
|
request = functions.upload.SaveFilePartRequest(
|
||||||
file_id, part_index, part)
|
file_id, part_index, part)
|
||||||
|
|
||||||
result = await self(request)
|
result = self(request)
|
||||||
if result:
|
if result:
|
||||||
__log__.debug('Uploaded %d/%d', part_index + 1,
|
__log__.debug('Uploaded %d/%d', part_index + 1,
|
||||||
part_count)
|
part_count)
|
||||||
|
@ -356,7 +356,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
async def _file_to_media(
|
def _file_to_media(
|
||||||
self, file, force_document=False,
|
self, file, force_document=False,
|
||||||
progress_callback=None, attributes=None, thumb=None,
|
progress_callback=None, attributes=None, thumb=None,
|
||||||
allow_cache=True, voice_note=False, video_note=False):
|
allow_cache=True, voice_note=False, video_note=False):
|
||||||
|
@ -387,7 +387,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
else:
|
else:
|
||||||
media = types.InputMediaDocumentExternal(file)
|
media = types.InputMediaDocumentExternal(file)
|
||||||
else:
|
else:
|
||||||
file_handle = await self.upload_file(
|
file_handle = self.upload_file(
|
||||||
file, progress_callback=progress_callback,
|
file, progress_callback=progress_callback,
|
||||||
use_cache=use_cache if allow_cache else None
|
use_cache=use_cache if allow_cache else None
|
||||||
)
|
)
|
||||||
|
@ -476,7 +476,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
||||||
|
|
||||||
input_kw = {}
|
input_kw = {}
|
||||||
if thumb:
|
if thumb:
|
||||||
input_kw['thumb'] = await self.upload_file(thumb)
|
input_kw['thumb'] = self.upload_file(thumb)
|
||||||
|
|
||||||
media = types.InputMediaUploadedDocument(
|
media = types.InputMediaUploadedDocument(
|
||||||
file=file_handle,
|
file=file_handle,
|
||||||
|
|
|
@ -12,11 +12,11 @@ _NOT_A_REQUEST = TypeError('You can only invoke requests, not types!')
|
||||||
|
|
||||||
|
|
||||||
class UserMethods(TelegramBaseClient):
|
class UserMethods(TelegramBaseClient):
|
||||||
async def __call__(self, request, ordered=False):
|
def __call__(self, request, ordered=False):
|
||||||
for r in (request if utils.is_list_like(request) else (request,)):
|
for r in (request if utils.is_list_like(request) else (request,)):
|
||||||
if not isinstance(r, TLRequest):
|
if not isinstance(r, TLRequest):
|
||||||
raise _NOT_A_REQUEST
|
raise _NOT_A_REQUEST
|
||||||
await r.resolve(self, utils)
|
r.resolve(self, utils)
|
||||||
|
|
||||||
self._last_request = time.time()
|
self._last_request = time.time()
|
||||||
for _ in range(self._request_retries):
|
for _ in range(self._request_retries):
|
||||||
|
@ -25,12 +25,12 @@ class UserMethods(TelegramBaseClient):
|
||||||
if isinstance(future, list):
|
if isinstance(future, list):
|
||||||
results = []
|
results = []
|
||||||
for f in future:
|
for f in future:
|
||||||
result = await f
|
result = f
|
||||||
self.session.process_entities(result)
|
self.session.process_entities(result)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
return results
|
return results
|
||||||
else:
|
else:
|
||||||
result = await future
|
result = future
|
||||||
self.session.process_entities(result)
|
self.session.process_entities(result)
|
||||||
return result
|
return result
|
||||||
except (errors.ServerError, errors.RpcCallFailError) as e:
|
except (errors.ServerError, errors.RpcCallFailError) as e:
|
||||||
|
@ -39,7 +39,7 @@ class UserMethods(TelegramBaseClient):
|
||||||
except (errors.FloodWaitError, errors.FloodTestPhoneWaitError) as e:
|
except (errors.FloodWaitError, errors.FloodTestPhoneWaitError) as e:
|
||||||
if e.seconds <= self.flood_sleep_threshold:
|
if e.seconds <= self.flood_sleep_threshold:
|
||||||
__log__.info('Sleeping for %ds on flood wait', e.seconds)
|
__log__.info('Sleeping for %ds on flood wait', e.seconds)
|
||||||
await asyncio.sleep(e.seconds, loop=self._loop)
|
asyncio.sleep(e.seconds, loop=self._loop)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
except (errors.PhoneMigrateError, errors.NetworkMigrateError,
|
except (errors.PhoneMigrateError, errors.NetworkMigrateError,
|
||||||
|
@ -48,15 +48,15 @@ class UserMethods(TelegramBaseClient):
|
||||||
should_raise = isinstance(e, (
|
should_raise = isinstance(e, (
|
||||||
errors.PhoneMigrateError, errors.NetworkMigrateError
|
errors.PhoneMigrateError, errors.NetworkMigrateError
|
||||||
))
|
))
|
||||||
if should_raise and await self.is_user_authorized():
|
if should_raise and self.is_user_authorized():
|
||||||
raise
|
raise
|
||||||
await self._switch_dc(e.new_dc)
|
self._switch_dc(e.new_dc)
|
||||||
|
|
||||||
raise ValueError('Number of retries reached 0')
|
raise ValueError('Number of retries reached 0')
|
||||||
|
|
||||||
# region Public methods
|
# region Public methods
|
||||||
|
|
||||||
async def get_me(self, input_peer=False):
|
def get_me(self, input_peer=False):
|
||||||
"""
|
"""
|
||||||
Gets "me" (the self user) which is currently authenticated,
|
Gets "me" (the self user) which is currently authenticated,
|
||||||
or None if the request fails (hence, not authenticated).
|
or None if the request fails (hence, not authenticated).
|
||||||
|
@ -74,7 +74,7 @@ class UserMethods(TelegramBaseClient):
|
||||||
return self._self_input_peer
|
return self._self_input_peer
|
||||||
|
|
||||||
try:
|
try:
|
||||||
me = (await self(
|
me = (self(
|
||||||
functions.users.GetUsersRequest([types.InputUserSelf()])))[0]
|
functions.users.GetUsersRequest([types.InputUserSelf()])))[0]
|
||||||
|
|
||||||
if not self._self_input_peer:
|
if not self._self_input_peer:
|
||||||
|
@ -86,7 +86,7 @@ class UserMethods(TelegramBaseClient):
|
||||||
except errors.UnauthorizedError:
|
except errors.UnauthorizedError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def is_user_authorized(self):
|
def is_user_authorized(self):
|
||||||
"""
|
"""
|
||||||
Returns ``True`` if the user is authorized.
|
Returns ``True`` if the user is authorized.
|
||||||
"""
|
"""
|
||||||
|
@ -94,12 +94,12 @@ class UserMethods(TelegramBaseClient):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._state = await self(functions.updates.GetStateRequest())
|
self._state = self(functions.updates.GetStateRequest())
|
||||||
return True
|
return True
|
||||||
except errors.RPCError:
|
except errors.RPCError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def get_entity(self, entity):
|
def get_entity(self, entity):
|
||||||
"""
|
"""
|
||||||
Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat`
|
Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat`
|
||||||
or :tl:`Channel`. You can also pass a list or iterable of entities,
|
or :tl:`Channel`. You can also pass a list or iterable of entities,
|
||||||
|
@ -144,7 +144,7 @@ class UserMethods(TelegramBaseClient):
|
||||||
if isinstance(x, str):
|
if isinstance(x, str):
|
||||||
inputs.append(x)
|
inputs.append(x)
|
||||||
else:
|
else:
|
||||||
inputs.append(await self.get_input_entity(x))
|
inputs.append(self.get_input_entity(x))
|
||||||
|
|
||||||
users = [x for x in inputs
|
users = [x for x in inputs
|
||||||
if isinstance(x, (types.InputPeerUser, types.InputPeerSelf))]
|
if isinstance(x, (types.InputPeerUser, types.InputPeerSelf))]
|
||||||
|
@ -157,13 +157,13 @@ class UserMethods(TelegramBaseClient):
|
||||||
tmp = []
|
tmp = []
|
||||||
while users:
|
while users:
|
||||||
curr, users = users[:200], users[200:]
|
curr, users = users[:200], users[200:]
|
||||||
tmp.extend(await self(functions.users.GetUsersRequest(curr)))
|
tmp.extend(self(functions.users.GetUsersRequest(curr)))
|
||||||
users = tmp
|
users = tmp
|
||||||
if chats: # TODO Handle chats slice?
|
if chats: # TODO Handle chats slice?
|
||||||
chats = (await self(
|
chats = (self(
|
||||||
functions.messages.GetChatsRequest(chats))).chats
|
functions.messages.GetChatsRequest(chats))).chats
|
||||||
if channels:
|
if channels:
|
||||||
channels = (await self(
|
channels = (self(
|
||||||
functions.channels.GetChannelsRequest(channels))).chats
|
functions.channels.GetChannelsRequest(channels))).chats
|
||||||
|
|
||||||
# Merge users, chats and channels into a single dictionary
|
# Merge users, chats and channels into a single dictionary
|
||||||
|
@ -179,7 +179,7 @@ class UserMethods(TelegramBaseClient):
|
||||||
result = []
|
result = []
|
||||||
for x in inputs:
|
for x in inputs:
|
||||||
if isinstance(x, str):
|
if isinstance(x, str):
|
||||||
result.append(await self._get_entity_from_string(x))
|
result.append(self._get_entity_from_string(x))
|
||||||
elif not isinstance(x, types.InputPeerSelf):
|
elif not isinstance(x, types.InputPeerSelf):
|
||||||
result.append(id_entity[utils.get_peer_id(x)])
|
result.append(id_entity[utils.get_peer_id(x)])
|
||||||
else:
|
else:
|
||||||
|
@ -190,7 +190,7 @@ class UserMethods(TelegramBaseClient):
|
||||||
|
|
||||||
return result[0] if single else result
|
return result[0] if single else result
|
||||||
|
|
||||||
async def get_input_entity(self, peer):
|
def get_input_entity(self, peer):
|
||||||
"""
|
"""
|
||||||
Turns the given peer into its input entity version. Most requests
|
Turns the given peer into its input entity version. Most requests
|
||||||
use this kind of :tl:`InputPeer`, so this is the most suitable call
|
use this kind of :tl:`InputPeer`, so this is the most suitable call
|
||||||
|
@ -260,7 +260,7 @@ class UserMethods(TelegramBaseClient):
|
||||||
|
|
||||||
if isinstance(peer, str):
|
if isinstance(peer, str):
|
||||||
return utils.get_input_peer(
|
return utils.get_input_peer(
|
||||||
await self._get_entity_from_string(peer))
|
self._get_entity_from_string(peer))
|
||||||
|
|
||||||
if not isinstance(peer, int) and (not isinstance(peer, TLObject)
|
if not isinstance(peer, int) and (not isinstance(peer, TLObject)
|
||||||
or peer.SUBCLASS_OF_ID != 0x2d45687):
|
or peer.SUBCLASS_OF_ID != 0x2d45687):
|
||||||
|
@ -280,7 +280,7 @@ class UserMethods(TelegramBaseClient):
|
||||||
|
|
||||||
# region Private methods
|
# region Private methods
|
||||||
|
|
||||||
async def _get_entity_from_string(self, string):
|
def _get_entity_from_string(self, string):
|
||||||
"""
|
"""
|
||||||
Gets a full entity from the given string, which may be a phone or
|
Gets a full entity from the given string, which may be a phone or
|
||||||
an username, and processes all the found entities on the session.
|
an username, and processes all the found entities on the session.
|
||||||
|
@ -294,14 +294,14 @@ class UserMethods(TelegramBaseClient):
|
||||||
"""
|
"""
|
||||||
phone = utils.parse_phone(string)
|
phone = utils.parse_phone(string)
|
||||||
if phone:
|
if phone:
|
||||||
for user in (await self(
|
for user in (self(
|
||||||
functions.contacts.GetContactsRequest(0))).users:
|
functions.contacts.GetContactsRequest(0))).users:
|
||||||
if user.phone == phone:
|
if user.phone == phone:
|
||||||
return user
|
return user
|
||||||
else:
|
else:
|
||||||
username, is_join_chat = utils.parse_username(string)
|
username, is_join_chat = utils.parse_username(string)
|
||||||
if is_join_chat:
|
if is_join_chat:
|
||||||
invite = await self(
|
invite = self(
|
||||||
functions.messages.CheckChatInviteRequest(username))
|
functions.messages.CheckChatInviteRequest(username))
|
||||||
|
|
||||||
if isinstance(invite, types.ChatInvite):
|
if isinstance(invite, types.ChatInvite):
|
||||||
|
@ -313,10 +313,10 @@ class UserMethods(TelegramBaseClient):
|
||||||
return invite.chat
|
return invite.chat
|
||||||
elif username:
|
elif username:
|
||||||
if username in ('me', 'self'):
|
if username in ('me', 'self'):
|
||||||
return await self.get_me()
|
return self.get_me()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await self(
|
result = self(
|
||||||
functions.contacts.ResolveUsernameRequest(username))
|
functions.contacts.ResolveUsernameRequest(username))
|
||||||
except errors.UsernameNotOccupiedError as e:
|
except errors.UsernameNotOccupiedError as e:
|
||||||
raise ValueError('No user has "{}" as username'
|
raise ValueError('No user has "{}" as username'
|
||||||
|
@ -328,7 +328,7 @@ class UserMethods(TelegramBaseClient):
|
||||||
return entity
|
return entity
|
||||||
try:
|
try:
|
||||||
# Nobody with this username, maybe it's an exact name/title
|
# Nobody with this username, maybe it's an exact name/title
|
||||||
return await self.get_entity(
|
return self.get_entity(
|
||||||
self.session.get_input_entity(string))
|
self.session.get_input_entity(string))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -30,7 +30,7 @@ class CdnDecrypter:
|
||||||
self.cdn_file_hashes = cdn_file_hashes
|
self.cdn_file_hashes = cdn_file_hashes
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def prepare_decrypter(client, cdn_client, cdn_redirect):
|
def prepare_decrypter(client, cdn_client, cdn_redirect):
|
||||||
"""
|
"""
|
||||||
Prepares a new CDN decrypter.
|
Prepares a new CDN decrypter.
|
||||||
|
|
||||||
|
@ -52,14 +52,14 @@ class CdnDecrypter:
|
||||||
cdn_aes, cdn_redirect.cdn_file_hashes
|
cdn_aes, cdn_redirect.cdn_file_hashes
|
||||||
)
|
)
|
||||||
|
|
||||||
cdn_file = await cdn_client(GetCdnFileRequest(
|
cdn_file = cdn_client(GetCdnFileRequest(
|
||||||
file_token=cdn_redirect.file_token,
|
file_token=cdn_redirect.file_token,
|
||||||
offset=cdn_redirect.cdn_file_hashes[0].offset,
|
offset=cdn_redirect.cdn_file_hashes[0].offset,
|
||||||
limit=cdn_redirect.cdn_file_hashes[0].limit
|
limit=cdn_redirect.cdn_file_hashes[0].limit
|
||||||
))
|
))
|
||||||
if isinstance(cdn_file, CdnFileReuploadNeeded):
|
if isinstance(cdn_file, CdnFileReuploadNeeded):
|
||||||
# We need to use the original client here
|
# We need to use the original client here
|
||||||
await client(ReuploadCdnFileRequest(
|
client(ReuploadCdnFileRequest(
|
||||||
file_token=cdn_redirect.file_token,
|
file_token=cdn_redirect.file_token,
|
||||||
request_token=cdn_file.request_token
|
request_token=cdn_file.request_token
|
||||||
))
|
))
|
||||||
|
|
|
@ -166,16 +166,16 @@ class ChatAction(EventBuilder):
|
||||||
self.action_message = custom.Message(
|
self.action_message = custom.Message(
|
||||||
client, self.action_message, self._entities, None)
|
client, self.action_message, self._entities, None)
|
||||||
|
|
||||||
async def respond(self, *args, **kwargs):
|
def respond(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Responds to the chat action message (not as a reply). Shorthand for
|
Responds to the chat action message (not as a reply). Shorthand for
|
||||||
`telethon.telegram_client.TelegramClient.send_message` with
|
`telethon.telegram_client.TelegramClient.send_message` with
|
||||||
``entity`` already set.
|
``entity`` already set.
|
||||||
"""
|
"""
|
||||||
return await self._client.send_message(
|
return self._client.send_message(
|
||||||
await self.get_input_chat(), *args, **kwargs)
|
self.get_input_chat(), *args, **kwargs)
|
||||||
|
|
||||||
async def reply(self, *args, **kwargs):
|
def reply(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Replies to the chat action message (as a reply). Shorthand for
|
Replies to the chat action message (as a reply). Shorthand for
|
||||||
`telethon.telegram_client.TelegramClient.send_message` with
|
`telethon.telegram_client.TelegramClient.send_message` with
|
||||||
|
@ -184,13 +184,13 @@ class ChatAction(EventBuilder):
|
||||||
Has the same effect as `respond` if there is no message.
|
Has the same effect as `respond` if there is no message.
|
||||||
"""
|
"""
|
||||||
if not self.action_message:
|
if not self.action_message:
|
||||||
return await self.respond(*args, **kwargs)
|
return self.respond(*args, **kwargs)
|
||||||
|
|
||||||
kwargs['reply_to'] = self.action_message.id
|
kwargs['reply_to'] = self.action_message.id
|
||||||
return await self._client.send_message(
|
return self._client.send_message(
|
||||||
await self.get_input_chat(), *args, **kwargs)
|
self.get_input_chat(), *args, **kwargs)
|
||||||
|
|
||||||
async def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Deletes the chat action message. You're responsible for checking
|
Deletes the chat action message. You're responsible for checking
|
||||||
whether you have the permission to do so, or to except the error
|
whether you have the permission to do so, or to except the error
|
||||||
|
@ -203,12 +203,12 @@ class ChatAction(EventBuilder):
|
||||||
if not self.action_message:
|
if not self.action_message:
|
||||||
return
|
return
|
||||||
|
|
||||||
return await self._client.delete_messages(
|
return self._client.delete_messages(
|
||||||
await self.get_input_chat(), [self.action_message],
|
self.get_input_chat(), [self.action_message],
|
||||||
*args, **kwargs
|
*args, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_pinned_message(self):
|
def get_pinned_message(self):
|
||||||
"""
|
"""
|
||||||
If ``new_pin`` is ``True``, this returns the
|
If ``new_pin`` is ``True``, this returns the
|
||||||
`telethon.tl.custom.message.Message` object that was pinned.
|
`telethon.tl.custom.message.Message` object that was pinned.
|
||||||
|
@ -217,8 +217,8 @@ class ChatAction(EventBuilder):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance(self._pinned_message, int)\
|
if isinstance(self._pinned_message, int)\
|
||||||
and await self.get_input_chat():
|
and self.get_input_chat():
|
||||||
r = await self._client(functions.channels.GetMessagesRequest(
|
r = self._client(functions.channels.GetMessagesRequest(
|
||||||
self._input_chat, [self._pinned_message]
|
self._input_chat, [self._pinned_message]
|
||||||
))
|
))
|
||||||
try:
|
try:
|
||||||
|
@ -245,12 +245,12 @@ class ChatAction(EventBuilder):
|
||||||
|
|
||||||
return self._added_by
|
return self._added_by
|
||||||
|
|
||||||
async def get_added_by(self):
|
def get_added_by(self):
|
||||||
"""
|
"""
|
||||||
Returns `added_by` but will make an API call if necessary.
|
Returns `added_by` but will make an API call if necessary.
|
||||||
"""
|
"""
|
||||||
if not self.added_by and self._added_by:
|
if not self.added_by and self._added_by:
|
||||||
self._added_by = await self._client.get_entity(self._added_by)
|
self._added_by = self._client.get_entity(self._added_by)
|
||||||
|
|
||||||
return self._added_by
|
return self._added_by
|
||||||
|
|
||||||
|
@ -266,12 +266,12 @@ class ChatAction(EventBuilder):
|
||||||
|
|
||||||
return self._kicked_by
|
return self._kicked_by
|
||||||
|
|
||||||
async def get_kicked_by(self):
|
def get_kicked_by(self):
|
||||||
"""
|
"""
|
||||||
Returns `kicked_by` but will make an API call if necessary.
|
Returns `kicked_by` but will make an API call if necessary.
|
||||||
"""
|
"""
|
||||||
if not self.kicked_by and self._kicked_by:
|
if not self.kicked_by and self._kicked_by:
|
||||||
self._kicked_by = await self._client.get_entity(self._kicked_by)
|
self._kicked_by = self._client.get_entity(self._kicked_by)
|
||||||
|
|
||||||
return self._kicked_by
|
return self._kicked_by
|
||||||
|
|
||||||
|
@ -286,11 +286,11 @@ class ChatAction(EventBuilder):
|
||||||
if self.users:
|
if self.users:
|
||||||
return self._users[0]
|
return self._users[0]
|
||||||
|
|
||||||
async def get_user(self):
|
def get_user(self):
|
||||||
"""
|
"""
|
||||||
Returns `user` but will make an API call if necessary.
|
Returns `user` but will make an API call if necessary.
|
||||||
"""
|
"""
|
||||||
if self.users or await self.get_users():
|
if self.users or self.get_users():
|
||||||
return self._users[0]
|
return self._users[0]
|
||||||
|
|
||||||
def input_user(self):
|
def input_user(self):
|
||||||
|
@ -300,11 +300,11 @@ class ChatAction(EventBuilder):
|
||||||
if self.input_users:
|
if self.input_users:
|
||||||
return self._input_users[0]
|
return self._input_users[0]
|
||||||
|
|
||||||
async def get_input_user(self):
|
def get_input_user(self):
|
||||||
"""
|
"""
|
||||||
Returns `input_user` but will make an API call if necessary.
|
Returns `input_user` but will make an API call if necessary.
|
||||||
"""
|
"""
|
||||||
if self.input_users or await self.get_input_users():
|
if self.input_users or self.get_input_users():
|
||||||
return self._input_users[0]
|
return self._input_users[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -335,7 +335,7 @@ class ChatAction(EventBuilder):
|
||||||
|
|
||||||
return self._users
|
return self._users
|
||||||
|
|
||||||
async def get_users(self):
|
def get_users(self):
|
||||||
"""
|
"""
|
||||||
Returns `users` but will make an API call if necessary.
|
Returns `users` but will make an API call if necessary.
|
||||||
"""
|
"""
|
||||||
|
@ -352,7 +352,7 @@ class ChatAction(EventBuilder):
|
||||||
missing.append(peer)
|
missing.append(peer)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
missing = await self._client.get_entity(missing)
|
missing = self._client.get_entity(missing)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
missing = []
|
missing = []
|
||||||
|
|
||||||
|
@ -376,7 +376,7 @@ class ChatAction(EventBuilder):
|
||||||
pass
|
pass
|
||||||
return self._input_users or []
|
return self._input_users or []
|
||||||
|
|
||||||
async def get_input_users(self):
|
def get_input_users(self):
|
||||||
"""
|
"""
|
||||||
Returns `input_users` but will make an API call if necessary.
|
Returns `input_users` but will make an API call if necessary.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -5,7 +5,7 @@ from .. import utils
|
||||||
from ..tl import TLObject, types
|
from ..tl import TLObject, types
|
||||||
|
|
||||||
|
|
||||||
async def _into_id_set(client, chats):
|
def _into_id_set(client, chats):
|
||||||
"""Helper util to turn the input chat or chats into a set of IDs."""
|
"""Helper util to turn the input chat or chats into a set of IDs."""
|
||||||
if chats is None:
|
if chats is None:
|
||||||
return None
|
return None
|
||||||
|
@ -28,9 +28,9 @@ async def _into_id_set(client, chats):
|
||||||
# 0x2d45687 == crc32(b'Peer')
|
# 0x2d45687 == crc32(b'Peer')
|
||||||
result.add(utils.get_peer_id(chat))
|
result.add(utils.get_peer_id(chat))
|
||||||
else:
|
else:
|
||||||
chat = await client.get_input_entity(chat)
|
chat = client.get_input_entity(chat)
|
||||||
if isinstance(chat, types.InputPeerSelf):
|
if isinstance(chat, types.InputPeerSelf):
|
||||||
chat = await client.get_me(input_peer=True)
|
chat = client.get_me(input_peer=True)
|
||||||
result.add(utils.get_peer_id(chat))
|
result.add(utils.get_peer_id(chat))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -60,10 +60,10 @@ class EventBuilder(abc.ABC):
|
||||||
def build(self, update):
|
def build(self, update):
|
||||||
"""Builds an event for the given update if possible, or returns None"""
|
"""Builds an event for the given update if possible, or returns None"""
|
||||||
|
|
||||||
async def resolve(self, client):
|
def resolve(self, client):
|
||||||
"""Helper method to allow event builders to be resolved before usage"""
|
"""Helper method to allow event builders to be resolved before usage"""
|
||||||
self.chats = await _into_id_set(client, self.chats)
|
self.chats = _into_id_set(client, self.chats)
|
||||||
self._self_id = (await client.get_me(input_peer=True)).user_id
|
self._self_id = (client.get_me(input_peer=True)).user_id
|
||||||
|
|
||||||
def _filter_event(self, event):
|
def _filter_event(self, event):
|
||||||
"""
|
"""
|
||||||
|
@ -130,7 +130,7 @@ class EventCommon(abc.ABC):
|
||||||
|
|
||||||
return self._input_chat
|
return self._input_chat
|
||||||
|
|
||||||
async def get_input_chat(self):
|
def get_input_chat(self):
|
||||||
"""
|
"""
|
||||||
Returns `input_chat`, but will make an API call to find the
|
Returns `input_chat`, but will make an API call to find the
|
||||||
input chat unless it's already cached.
|
input chat unless it's already cached.
|
||||||
|
@ -138,13 +138,13 @@ class EventCommon(abc.ABC):
|
||||||
if self.input_chat is None and self._chat_peer is not None:
|
if self.input_chat is None and self._chat_peer is not None:
|
||||||
ch = isinstance(self._chat_peer, types.PeerChannel)
|
ch = isinstance(self._chat_peer, types.PeerChannel)
|
||||||
if not ch and self._message_id is not None:
|
if not ch and self._message_id is not None:
|
||||||
msg = await self._client.get_messages(
|
msg = self._client.get_messages(
|
||||||
None, ids=self._message_id)
|
None, ids=self._message_id)
|
||||||
self._chat = msg._chat
|
self._chat = msg._chat
|
||||||
self._input_chat = msg._input_chat
|
self._input_chat = msg._input_chat
|
||||||
else:
|
else:
|
||||||
target = utils.get_peer_id(self._chat_peer)
|
target = utils.get_peer_id(self._chat_peer)
|
||||||
async for d in self._client.iter_dialogs(100):
|
for d in self._client.iter_dialogs(100):
|
||||||
if d.id == target:
|
if d.id == target:
|
||||||
self._chat = d.entity
|
self._chat = d.entity
|
||||||
self._input_chat = d.input_entity
|
self._input_chat = d.input_entity
|
||||||
|
@ -179,15 +179,15 @@ class EventCommon(abc.ABC):
|
||||||
|
|
||||||
return self._chat
|
return self._chat
|
||||||
|
|
||||||
async def get_chat(self):
|
def get_chat(self):
|
||||||
"""
|
"""
|
||||||
Returns `chat`, but will make an API call to find the
|
Returns `chat`, but will make an API call to find the
|
||||||
chat unless it's already cached.
|
chat unless it's already cached.
|
||||||
"""
|
"""
|
||||||
if self.chat is None and await self.get_input_chat():
|
if self.chat is None and self.get_input_chat():
|
||||||
try:
|
try:
|
||||||
self._chat =\
|
self._chat =\
|
||||||
await self._client.get_entity(self._input_chat)
|
self._client.get_entity(self._input_chat)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return self._chat
|
return self._chat
|
||||||
|
|
|
@ -88,7 +88,7 @@ class MessageRead(EventBuilder):
|
||||||
"""
|
"""
|
||||||
return self._message_ids
|
return self._message_ids
|
||||||
|
|
||||||
async def get_messages(self):
|
def get_messages(self):
|
||||||
"""
|
"""
|
||||||
Returns the list of `telethon.tl.custom.message.Message`
|
Returns the list of `telethon.tl.custom.message.Message`
|
||||||
**which contents'** were read.
|
**which contents'** were read.
|
||||||
|
@ -97,11 +97,11 @@ class MessageRead(EventBuilder):
|
||||||
was read instead checking if it's in here.
|
was read instead checking if it's in here.
|
||||||
"""
|
"""
|
||||||
if self._messages is None:
|
if self._messages is None:
|
||||||
chat = await self.get_input_chat()
|
chat = self.get_input_chat()
|
||||||
if not chat:
|
if not chat:
|
||||||
self._messages = []
|
self._messages = []
|
||||||
else:
|
else:
|
||||||
self._messages = await self._client.get_messages(
|
self._messages = self._client.get_messages(
|
||||||
chat, ids=self._message_ids)
|
chat, ids=self._message_ids)
|
||||||
|
|
||||||
return self._messages
|
return self._messages
|
||||||
|
|
|
@ -71,9 +71,9 @@ class NewMessage(EventBuilder):
|
||||||
self.from_users, self.forwards, self.from_users
|
self.from_users, self.forwards, self.from_users
|
||||||
))
|
))
|
||||||
|
|
||||||
async def resolve(self, client):
|
def resolve(self, client):
|
||||||
await super().resolve(client)
|
super().resolve(client)
|
||||||
self.from_users = await _into_id_set(client, self.from_users)
|
self.from_users = _into_id_set(client, self.from_users)
|
||||||
|
|
||||||
def build(self, update):
|
def build(self, update):
|
||||||
if isinstance(update,
|
if isinstance(update,
|
||||||
|
@ -179,7 +179,7 @@ class NewMessage(EventBuilder):
|
||||||
>>> client = TelegramClient(...)
|
>>> client = TelegramClient(...)
|
||||||
>>>
|
>>>
|
||||||
>>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!'))
|
>>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!'))
|
||||||
... async def handler(event):
|
... def handler(event):
|
||||||
... # In this case, the result is a ``Match`` object
|
... # In this case, the result is a ``Match`` object
|
||||||
... # since the ``str`` pattern was converted into
|
... # since the ``str`` pattern was converted into
|
||||||
... # the ``re.compile(pattern).match`` function.
|
... # the ``re.compile(pattern).match`` function.
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Raw(EventBuilder):
|
||||||
assert all(isinstance(x, type) for x in types)
|
assert all(isinstance(x, type) for x in types)
|
||||||
self.types = tuple(types)
|
self.types = tuple(types)
|
||||||
|
|
||||||
async def resolve(self, client):
|
def resolve(self, client):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def build(self, update):
|
def build(self, update):
|
||||||
|
|
|
@ -152,18 +152,18 @@ class UserUpdate(EventBuilder):
|
||||||
"""Alias for `chat` (conversation)."""
|
"""Alias for `chat` (conversation)."""
|
||||||
return self.chat
|
return self.chat
|
||||||
|
|
||||||
async def get_user(self):
|
def get_user(self):
|
||||||
"""Alias for `get_chat` (conversation)."""
|
"""Alias for `get_chat` (conversation)."""
|
||||||
return await self.get_chat()
|
return self.get_chat()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def input_user(self):
|
def input_user(self):
|
||||||
"""Alias for `input_chat`."""
|
"""Alias for `input_chat`."""
|
||||||
return self.input_chat
|
return self.input_chat
|
||||||
|
|
||||||
async def get_input_user(self):
|
def get_input_user(self):
|
||||||
"""Alias for `get_input_chat`."""
|
"""Alias for `get_input_chat`."""
|
||||||
return await self.get_input_chat()
|
return self.get_input_chat()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_id(self):
|
def user_id(self):
|
||||||
|
|
|
@ -71,7 +71,7 @@ class TcpClient:
|
||||||
s.setblocking(False)
|
s.setblocking(False)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
async def connect(self, ip, port):
|
def connect(self, ip, port):
|
||||||
"""
|
"""
|
||||||
Tries connecting to IP:port unless an OSError is raised.
|
Tries connecting to IP:port unless an OSError is raised.
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ class TcpClient:
|
||||||
if self._socket is None:
|
if self._socket is None:
|
||||||
self._socket = self._create_socket(mode, self.proxy)
|
self._socket = self._create_socket(mode, self.proxy)
|
||||||
|
|
||||||
await asyncio.wait_for(
|
asyncio.wait_for(
|
||||||
self._loop.sock_connect(self._socket, address),
|
self._loop.sock_connect(self._socket, address),
|
||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
loop=self._loop
|
loop=self._loop
|
||||||
|
@ -122,12 +122,12 @@ class TcpClient:
|
||||||
if fd:
|
if fd:
|
||||||
self._loop.remove_reader(fd)
|
self._loop.remove_reader(fd)
|
||||||
|
|
||||||
async def _wait_timeout_or_close(self, coro):
|
def _wait_timeout_or_close(self, coro):
|
||||||
"""
|
"""
|
||||||
Waits for the given coroutine to complete unless
|
Waits for the given coroutine to complete unless
|
||||||
the socket is closed or `self.timeout` expires.
|
the socket is closed or `self.timeout` expires.
|
||||||
"""
|
"""
|
||||||
done, running = await asyncio.wait(
|
done, running = asyncio.wait(
|
||||||
[coro, self._closed.wait()],
|
[coro, self._closed.wait()],
|
||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
return_when=asyncio.FIRST_COMPLETED,
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
|
@ -141,7 +141,7 @@ class TcpClient:
|
||||||
raise asyncio.TimeoutError()
|
raise asyncio.TimeoutError()
|
||||||
return done.pop().result()
|
return done.pop().result()
|
||||||
|
|
||||||
async def write(self, data):
|
def write(self, data):
|
||||||
"""
|
"""
|
||||||
Writes (sends) the specified bytes to the connected peer.
|
Writes (sends) the specified bytes to the connected peer.
|
||||||
:param data: the data to send.
|
:param data: the data to send.
|
||||||
|
@ -150,14 +150,14 @@ class TcpClient:
|
||||||
raise ConnectionResetError('Not connected')
|
raise ConnectionResetError('Not connected')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._wait_timeout_or_close(self.sock_sendall(data))
|
self._wait_timeout_or_close(self.sock_sendall(data))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.errno in CONN_RESET_ERRNOS:
|
if e.errno in CONN_RESET_ERRNOS:
|
||||||
raise ConnectionResetError() from e
|
raise ConnectionResetError() from e
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def read(self, size):
|
def read(self, size):
|
||||||
"""
|
"""
|
||||||
Reads (receives) a whole block of size bytes from the connected peer.
|
Reads (receives) a whole block of size bytes from the connected peer.
|
||||||
|
|
||||||
|
@ -171,7 +171,7 @@ class TcpClient:
|
||||||
bytes_left = size
|
bytes_left = size
|
||||||
while bytes_left != 0:
|
while bytes_left != 0:
|
||||||
try:
|
try:
|
||||||
partial = await self._wait_timeout_or_close(
|
partial = self._wait_timeout_or_close(
|
||||||
self.sock_recv(bytes_left)
|
self.sock_recv(bytes_left)
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
|
|
|
@ -19,7 +19,7 @@ from ..tl.functions import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def do_authentication(sender):
|
def do_authentication(sender):
|
||||||
"""
|
"""
|
||||||
Executes the authentication process with the Telegram servers.
|
Executes the authentication process with the Telegram servers.
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ async def do_authentication(sender):
|
||||||
"""
|
"""
|
||||||
# Step 1 sending: PQ Request, endianness doesn't matter since it's random
|
# Step 1 sending: PQ Request, endianness doesn't matter since it's random
|
||||||
nonce = int.from_bytes(os.urandom(16), 'big', signed=True)
|
nonce = int.from_bytes(os.urandom(16), 'big', signed=True)
|
||||||
res_pq = await sender.send(ReqPqMultiRequest(nonce))
|
res_pq = sender.send(ReqPqMultiRequest(nonce))
|
||||||
assert isinstance(res_pq, ResPQ), 'Step 1 answer was %s' % res_pq
|
assert isinstance(res_pq, ResPQ), 'Step 1 answer was %s' % res_pq
|
||||||
|
|
||||||
if res_pq.nonce != nonce:
|
if res_pq.nonce != nonce:
|
||||||
|
@ -64,7 +64,7 @@ async def do_authentication(sender):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
server_dh_params = await sender.send(ReqDHParamsRequest(
|
server_dh_params = sender.send(ReqDHParamsRequest(
|
||||||
nonce=res_pq.nonce,
|
nonce=res_pq.nonce,
|
||||||
server_nonce=res_pq.server_nonce,
|
server_nonce=res_pq.server_nonce,
|
||||||
p=p, q=q,
|
p=p, q=q,
|
||||||
|
@ -139,7 +139,7 @@ async def do_authentication(sender):
|
||||||
client_dh_encrypted = AES.encrypt_ige(client_dh_inner_hashed, key, iv)
|
client_dh_encrypted = AES.encrypt_ige(client_dh_inner_hashed, key, iv)
|
||||||
|
|
||||||
# Prepare Set client DH params
|
# Prepare Set client DH params
|
||||||
dh_gen = await sender.send(SetClientDHParamsRequest(
|
dh_gen = sender.send(SetClientDHParamsRequest(
|
||||||
nonce=res_pq.nonce,
|
nonce=res_pq.nonce,
|
||||||
server_nonce=res_pq.server_nonce,
|
server_nonce=res_pq.server_nonce,
|
||||||
encrypted_data=client_dh_encrypted,
|
encrypted_data=client_dh_encrypted,
|
||||||
|
|
|
@ -33,7 +33,7 @@ class Connection(abc.ABC):
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def connect(self, ip, port):
|
def connect(self, ip, port):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -51,7 +51,7 @@ class Connection(abc.ABC):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def close(self):
|
def close(self):
|
||||||
"""Closes the connection."""
|
"""Closes the connection."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -64,11 +64,11 @@ class Connection(abc.ABC):
|
||||||
)
|
)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def recv(self):
|
def recv(self):
|
||||||
"""Receives and unpacks a message"""
|
"""Receives and unpacks a message"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def send(self, message):
|
def send(self, message):
|
||||||
"""Encapsulates and sends the given message"""
|
"""Encapsulates and sends the given message"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
|
@ -9,23 +9,23 @@ class ConnectionTcpAbridged(ConnectionTcpFull):
|
||||||
only require 1 byte if the packet length is less than
|
only require 1 byte if the packet length is less than
|
||||||
508 bytes (127 << 2, which is very common).
|
508 bytes (127 << 2, which is very common).
|
||||||
"""
|
"""
|
||||||
async def connect(self, ip, port):
|
def connect(self, ip, port):
|
||||||
result = await super().connect(ip, port)
|
result = super().connect(ip, port)
|
||||||
await self.conn.write(b'\xef')
|
self.conn.write(b'\xef')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def recv(self):
|
def recv(self):
|
||||||
length = struct.unpack('<B', await self.read(1))[0]
|
length = struct.unpack('<B', self.read(1))[0]
|
||||||
if length >= 127:
|
if length >= 127:
|
||||||
length = struct.unpack('<i', await self.read(3) + b'\0')[0]
|
length = struct.unpack('<i', self.read(3) + b'\0')[0]
|
||||||
|
|
||||||
return await self.read(length << 2)
|
return self.read(length << 2)
|
||||||
|
|
||||||
async def send(self, message):
|
def send(self, message):
|
||||||
length = len(message) >> 2
|
length = len(message) >> 2
|
||||||
if length < 127:
|
if length < 127:
|
||||||
length = struct.pack('B', length)
|
length = struct.pack('B', length)
|
||||||
else:
|
else:
|
||||||
length = b'\x7f' + int.to_bytes(length, 3, 'little')
|
length = b'\x7f' + int.to_bytes(length, 3, 'little')
|
||||||
|
|
||||||
await self.write(length + message)
|
self.write(length + message)
|
||||||
|
|
|
@ -21,9 +21,9 @@ class ConnectionTcpFull(Connection):
|
||||||
self.read = self.conn.read
|
self.read = self.conn.read
|
||||||
self.write = self.conn.write
|
self.write = self.conn.write
|
||||||
|
|
||||||
async def connect(self, ip, port):
|
def connect(self, ip, port):
|
||||||
try:
|
try:
|
||||||
await self.conn.connect(ip, port)
|
self.conn.connect(ip, port)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.errno == errno.EISCONN:
|
if e.errno == errno.EISCONN:
|
||||||
return # Already connected, no need to re-set everything up
|
return # Already connected, no need to re-set everything up
|
||||||
|
@ -38,13 +38,13 @@ class ConnectionTcpFull(Connection):
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
return self.conn.is_connected
|
return self.conn.is_connected
|
||||||
|
|
||||||
async def close(self):
|
def close(self):
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
|
||||||
async def recv(self):
|
def recv(self):
|
||||||
packet_len_seq = await self.read(8) # 4 and 4
|
packet_len_seq = self.read(8) # 4 and 4
|
||||||
packet_len, seq = struct.unpack('<ii', packet_len_seq)
|
packet_len, seq = struct.unpack('<ii', packet_len_seq)
|
||||||
body = await self.read(packet_len - 8)
|
body = self.read(packet_len - 8)
|
||||||
checksum = struct.unpack('<I', body[-4:])[0]
|
checksum = struct.unpack('<I', body[-4:])[0]
|
||||||
body = body[:-4]
|
body = body[:-4]
|
||||||
|
|
||||||
|
@ -54,11 +54,11 @@ class ConnectionTcpFull(Connection):
|
||||||
|
|
||||||
return body
|
return body
|
||||||
|
|
||||||
async def send(self, message):
|
def send(self, message):
|
||||||
# https://core.telegram.org/mtproto#tcp-transport
|
# https://core.telegram.org/mtproto#tcp-transport
|
||||||
# total length, sequence number, packet and checksum (CRC32)
|
# total length, sequence number, packet and checksum (CRC32)
|
||||||
length = len(message) + 12
|
length = len(message) + 12
|
||||||
data = struct.pack('<ii', length, self._send_counter) + message
|
data = struct.pack('<ii', length, self._send_counter) + message
|
||||||
crc = struct.pack('<I', crc32(data))
|
crc = struct.pack('<I', crc32(data))
|
||||||
self._send_counter += 1
|
self._send_counter += 1
|
||||||
await self.write(data + crc)
|
self.write(data + crc)
|
||||||
|
|
|
@ -8,13 +8,13 @@ class ConnectionTcpIntermediate(ConnectionTcpFull):
|
||||||
Intermediate mode between `ConnectionTcpFull` and `ConnectionTcpAbridged`.
|
Intermediate mode between `ConnectionTcpFull` and `ConnectionTcpAbridged`.
|
||||||
Always sends 4 extra bytes for the packet length.
|
Always sends 4 extra bytes for the packet length.
|
||||||
"""
|
"""
|
||||||
async def connect(self, ip, port):
|
def connect(self, ip, port):
|
||||||
result = await super().connect(ip, port)
|
result = super().connect(ip, port)
|
||||||
await self.conn.write(b'\xee\xee\xee\xee')
|
self.conn.write(b'\xee\xee\xee\xee')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def recv(self):
|
def recv(self):
|
||||||
return await self.read(struct.unpack('<i', await self.read(4))[0])
|
return self.read(struct.unpack('<i', self.read(4))[0])
|
||||||
|
|
||||||
async def send(self, message):
|
def send(self, message):
|
||||||
await self.write(struct.pack('<i', len(message)) + message)
|
self.write(struct.pack('<i', len(message)) + message)
|
||||||
|
|
|
@ -17,8 +17,8 @@ class ConnectionTcpObfuscated(ConnectionTcpAbridged):
|
||||||
self.read = lambda s: self._aes_decrypt.encrypt(self.conn.read(s))
|
self.read = lambda s: self._aes_decrypt.encrypt(self.conn.read(s))
|
||||||
self.write = lambda d: self.conn.write(self._aes_encrypt.encrypt(d))
|
self.write = lambda d: self.conn.write(self._aes_encrypt.encrypt(d))
|
||||||
|
|
||||||
async def connect(self, ip, port):
|
def connect(self, ip, port):
|
||||||
result = await ConnectionTcpFull.connect(self, ip, port)
|
result = ConnectionTcpFull.connect(self, ip, port)
|
||||||
# Obfuscated messages secrets cannot start with any of these
|
# Obfuscated messages secrets cannot start with any of these
|
||||||
keywords = (b'PVrG', b'GET ', b'POST', b'\xee' * 4)
|
keywords = (b'PVrG', b'GET ', b'POST', b'\xee' * 4)
|
||||||
while True:
|
while True:
|
||||||
|
@ -42,5 +42,5 @@ class ConnectionTcpObfuscated(ConnectionTcpAbridged):
|
||||||
self._aes_decrypt = AESModeCTR(decrypt_key, decrypt_iv)
|
self._aes_decrypt = AESModeCTR(decrypt_key, decrypt_iv)
|
||||||
|
|
||||||
random[56:64] = self._aes_encrypt.encrypt(bytes(random))[56:64]
|
random[56:64] = self._aes_encrypt.encrypt(bytes(random))[56:64]
|
||||||
await self.conn.write(bytes(random))
|
self.conn.write(bytes(random))
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -23,17 +23,17 @@ class MTProtoPlainSender:
|
||||||
self._state = MTProtoState(auth_key=None)
|
self._state = MTProtoState(auth_key=None)
|
||||||
self._connection = connection
|
self._connection = connection
|
||||||
|
|
||||||
async def send(self, request):
|
def send(self, request):
|
||||||
"""
|
"""
|
||||||
Sends and receives the result for the given request.
|
Sends and receives the result for the given request.
|
||||||
"""
|
"""
|
||||||
body = bytes(request)
|
body = bytes(request)
|
||||||
msg_id = self._state._get_new_msg_id()
|
msg_id = self._state._get_new_msg_id()
|
||||||
await self._connection.send(
|
self._connection.send(
|
||||||
struct.pack('<QQi', 0, msg_id, len(body)) + body
|
struct.pack('<QQi', 0, msg_id, len(body)) + body
|
||||||
)
|
)
|
||||||
|
|
||||||
body = await self._connection.recv()
|
body = self._connection.recv()
|
||||||
if body == b'l\xfe\xff\xff': # -404 little endian signed
|
if body == b'l\xfe\xff\xff': # -404 little endian signed
|
||||||
# Broken authorization, must reset the auth key
|
# Broken authorization, must reset the auth key
|
||||||
raise BrokenAuthKeyError()
|
raise BrokenAuthKeyError()
|
||||||
|
|
|
@ -110,7 +110,7 @@ class MTProtoSender:
|
||||||
|
|
||||||
# Public API
|
# Public API
|
||||||
|
|
||||||
async def connect(self, ip, port):
|
def connect(self, ip, port):
|
||||||
"""
|
"""
|
||||||
Connects to the specified ``ip:port``, and generates a new
|
Connects to the specified ``ip:port``, and generates a new
|
||||||
authorization key for the `MTProtoSender.session` if it does
|
authorization key for the `MTProtoSender.session` if it does
|
||||||
|
@ -123,12 +123,12 @@ class MTProtoSender:
|
||||||
self._ip = ip
|
self._ip = ip
|
||||||
self._port = port
|
self._port = port
|
||||||
self._user_connected = True
|
self._user_connected = True
|
||||||
await self._connect()
|
self._connect()
|
||||||
|
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
return self._user_connected
|
return self._user_connected
|
||||||
|
|
||||||
async def disconnect(self):
|
def disconnect(self):
|
||||||
"""
|
"""
|
||||||
Cleanly disconnects the instance from the network, cancels
|
Cleanly disconnects the instance from the network, cancels
|
||||||
all pending requests, and closes the send and receive loops.
|
all pending requests, and closes the send and receive loops.
|
||||||
|
@ -137,14 +137,14 @@ class MTProtoSender:
|
||||||
__log__.info('User is already disconnected!')
|
__log__.info('User is already disconnected!')
|
||||||
return
|
return
|
||||||
|
|
||||||
await self._disconnect()
|
self._disconnect()
|
||||||
|
|
||||||
async def _disconnect(self, error=None):
|
def _disconnect(self, error=None):
|
||||||
__log__.info('Disconnecting from {}...'.format(self._ip))
|
__log__.info('Disconnecting from {}...'.format(self._ip))
|
||||||
self._user_connected = False
|
self._user_connected = False
|
||||||
try:
|
try:
|
||||||
__log__.debug('Closing current connection...')
|
__log__.debug('Closing current connection...')
|
||||||
await self._connection.close()
|
self._connection.close()
|
||||||
finally:
|
finally:
|
||||||
__log__.debug('Cancelling {} pending message(s)...'
|
__log__.debug('Cancelling {} pending message(s)...'
|
||||||
.format(len(self._pending_messages)))
|
.format(len(self._pending_messages)))
|
||||||
|
@ -183,11 +183,11 @@ class MTProtoSender:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
async def method():
|
def method():
|
||||||
# Sending (enqueued for the send loop)
|
# Sending (enqueued for the send loop)
|
||||||
future = sender.send(request)
|
future = sender.send(request)
|
||||||
# Receiving (waits for the receive loop to read the result)
|
# Receiving (waits for the receive loop to read the result)
|
||||||
result = await future
|
result = future
|
||||||
|
|
||||||
Designed like this because Telegram may send the response at
|
Designed like this because Telegram may send the response at
|
||||||
any point, and it can send other items while one waits for it.
|
any point, and it can send other items while one waits for it.
|
||||||
|
@ -196,7 +196,7 @@ class MTProtoSender:
|
||||||
would otherwise work.
|
would otherwise work.
|
||||||
|
|
||||||
Since the receiving part is "built in" the future, it's
|
Since the receiving part is "built in" the future, it's
|
||||||
impossible to await receive a result that was never sent.
|
impossible to receive a result that was never sent.
|
||||||
"""
|
"""
|
||||||
if not self._user_connected:
|
if not self._user_connected:
|
||||||
raise ConnectionError('Cannot send requests while disconnected')
|
raise ConnectionError('Cannot send requests while disconnected')
|
||||||
|
@ -230,7 +230,7 @@ class MTProtoSender:
|
||||||
|
|
||||||
# Private methods
|
# Private methods
|
||||||
|
|
||||||
async def _connect(self):
|
def _connect(self):
|
||||||
"""
|
"""
|
||||||
Performs the actual connection, retrying, generating the
|
Performs the actual connection, retrying, generating the
|
||||||
authorization key if necessary, and starting the send and
|
authorization key if necessary, and starting the send and
|
||||||
|
@ -240,7 +240,7 @@ class MTProtoSender:
|
||||||
for retry in range(1, self._retries + 1):
|
for retry in range(1, self._retries + 1):
|
||||||
try:
|
try:
|
||||||
__log__.debug('Connection attempt {}...'.format(retry))
|
__log__.debug('Connection attempt {}...'.format(retry))
|
||||||
await self._connection.connect(self._ip, self._port)
|
self._connection.connect(self._ip, self._port)
|
||||||
except (asyncio.TimeoutError, OSError) as e:
|
except (asyncio.TimeoutError, OSError) as e:
|
||||||
__log__.warning('Attempt {} at connecting failed: {}: {}'
|
__log__.warning('Attempt {} at connecting failed: {}: {}'
|
||||||
.format(retry, type(e).__name__, e))
|
.format(retry, type(e).__name__, e))
|
||||||
|
@ -257,7 +257,7 @@ class MTProtoSender:
|
||||||
try:
|
try:
|
||||||
__log__.debug('New auth_key attempt {}...'.format(retry))
|
__log__.debug('New auth_key attempt {}...'.format(retry))
|
||||||
self.state.auth_key, self.state.time_offset =\
|
self.state.auth_key, self.state.time_offset =\
|
||||||
await authenticator.do_authentication(plain)
|
authenticator.do_authentication(plain)
|
||||||
|
|
||||||
if self._auth_key_callback:
|
if self._auth_key_callback:
|
||||||
self._auth_key_callback(self.state.auth_key)
|
self._auth_key_callback(self.state.auth_key)
|
||||||
|
@ -269,7 +269,7 @@ class MTProtoSender:
|
||||||
else:
|
else:
|
||||||
e = ConnectionError('auth_key generation failed {} times'
|
e = ConnectionError('auth_key generation failed {} times'
|
||||||
.format(self._retries))
|
.format(self._retries))
|
||||||
await self._disconnect(error=e)
|
self._disconnect(error=e)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
__log__.debug('Starting send loop')
|
__log__.debug('Starting send loop')
|
||||||
|
@ -283,7 +283,7 @@ class MTProtoSender:
|
||||||
self._disconnected = asyncio.Future()
|
self._disconnected = asyncio.Future()
|
||||||
__log__.info('Connection to {} complete!'.format(self._ip))
|
__log__.info('Connection to {} complete!'.format(self._ip))
|
||||||
|
|
||||||
async def _reconnect(self):
|
def _reconnect(self):
|
||||||
"""
|
"""
|
||||||
Cleanly disconnects and then reconnects.
|
Cleanly disconnects and then reconnects.
|
||||||
"""
|
"""
|
||||||
|
@ -291,20 +291,20 @@ class MTProtoSender:
|
||||||
self._send_queue.put_nowait(_reconnect_sentinel)
|
self._send_queue.put_nowait(_reconnect_sentinel)
|
||||||
|
|
||||||
__log__.debug('Awaiting for the send loop before reconnecting...')
|
__log__.debug('Awaiting for the send loop before reconnecting...')
|
||||||
await self._send_loop_handle
|
self._send_loop_handle
|
||||||
|
|
||||||
__log__.debug('Awaiting for the receive loop before reconnecting...')
|
__log__.debug('Awaiting for the receive loop before reconnecting...')
|
||||||
await self._recv_loop_handle
|
self._recv_loop_handle
|
||||||
|
|
||||||
__log__.debug('Closing current connection...')
|
__log__.debug('Closing current connection...')
|
||||||
await self._connection.close()
|
self._connection.close()
|
||||||
|
|
||||||
self._reconnecting = False
|
self._reconnecting = False
|
||||||
|
|
||||||
retries = self._retries if self._auto_reconnect else 0
|
retries = self._retries if self._auto_reconnect else 0
|
||||||
for retry in range(1, retries + 1):
|
for retry in range(1, retries + 1):
|
||||||
try:
|
try:
|
||||||
await self._connect()
|
self._connect()
|
||||||
for m in self._pending_messages.values():
|
for m in self._pending_messages.values():
|
||||||
self._send_queue.put_nowait(m)
|
self._send_queue.put_nowait(m)
|
||||||
|
|
||||||
|
@ -316,7 +316,7 @@ class MTProtoSender:
|
||||||
__log__.info('Failed reconnection retry %d/%d', retry, retries)
|
__log__.info('Failed reconnection retry %d/%d', retry, retries)
|
||||||
else:
|
else:
|
||||||
__log__.error('Failed to reconnect automatically.')
|
__log__.error('Failed to reconnect automatically.')
|
||||||
await self._disconnect(error=ConnectionError())
|
self._disconnect(error=ConnectionError())
|
||||||
|
|
||||||
def _start_reconnect(self):
|
def _start_reconnect(self):
|
||||||
"""Starts a reconnection in the background."""
|
"""Starts a reconnection in the background."""
|
||||||
|
@ -342,7 +342,7 @@ class MTProtoSender:
|
||||||
|
|
||||||
# Loops
|
# Loops
|
||||||
|
|
||||||
async def _send_loop(self):
|
def _send_loop(self):
|
||||||
"""
|
"""
|
||||||
This loop is responsible for popping items off the send
|
This loop is responsible for popping items off the send
|
||||||
queue, encrypting them, and sending them over the network.
|
queue, encrypting them, and sending them over the network.
|
||||||
|
@ -357,7 +357,7 @@ class MTProtoSender:
|
||||||
self._send_queue.put_nowait(self._last_ack)
|
self._send_queue.put_nowait(self._last_ack)
|
||||||
self._pending_ack.clear()
|
self._pending_ack.clear()
|
||||||
|
|
||||||
messages = await self._send_queue.get()
|
messages = self._send_queue.get()
|
||||||
if messages == _reconnect_sentinel:
|
if messages == _reconnect_sentinel:
|
||||||
if self._reconnecting:
|
if self._reconnecting:
|
||||||
break
|
break
|
||||||
|
@ -381,7 +381,7 @@ class MTProtoSender:
|
||||||
while not any(m.future.cancelled() for m in messages):
|
while not any(m.future.cancelled() for m in messages):
|
||||||
try:
|
try:
|
||||||
__log__.debug('Sending {} bytes...'.format(len(body)))
|
__log__.debug('Sending {} bytes...'.format(len(body)))
|
||||||
await self._connection.send(body)
|
self._connection.send(body)
|
||||||
break
|
break
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
continue
|
continue
|
||||||
|
@ -394,7 +394,7 @@ class MTProtoSender:
|
||||||
__log__.warning('OSError while sending %s', e)
|
__log__.warning('OSError while sending %s', e)
|
||||||
else:
|
else:
|
||||||
__log__.exception('Unhandled exception while receiving')
|
__log__.exception('Unhandled exception while receiving')
|
||||||
await asyncio.sleep(1)
|
asyncio.sleep(1)
|
||||||
|
|
||||||
self._start_reconnect()
|
self._start_reconnect()
|
||||||
break
|
break
|
||||||
|
@ -411,7 +411,7 @@ class MTProtoSender:
|
||||||
__log__.debug('Outgoing messages {} sent!'
|
__log__.debug('Outgoing messages {} sent!'
|
||||||
.format(', '.join(str(m.msg_id) for m in messages)))
|
.format(', '.join(str(m.msg_id) for m in messages)))
|
||||||
|
|
||||||
async def _recv_loop(self):
|
def _recv_loop(self):
|
||||||
"""
|
"""
|
||||||
This loop is responsible for reading all incoming responses
|
This loop is responsible for reading all incoming responses
|
||||||
from the network, decrypting and handling or dispatching them.
|
from the network, decrypting and handling or dispatching them.
|
||||||
|
@ -421,7 +421,7 @@ class MTProtoSender:
|
||||||
while self._user_connected and not self._reconnecting:
|
while self._user_connected and not self._reconnecting:
|
||||||
try:
|
try:
|
||||||
__log__.debug('Receiving items from the network...')
|
__log__.debug('Receiving items from the network...')
|
||||||
body = await self._connection.recv()
|
body = self._connection.recv()
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
continue
|
continue
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
|
@ -433,7 +433,7 @@ class MTProtoSender:
|
||||||
__log__.warning('OSError while receiving %s', e)
|
__log__.warning('OSError while receiving %s', e)
|
||||||
else:
|
else:
|
||||||
__log__.exception('Unhandled exception while receiving')
|
__log__.exception('Unhandled exception while receiving')
|
||||||
await asyncio.sleep(1)
|
asyncio.sleep(1)
|
||||||
|
|
||||||
self._start_reconnect()
|
self._start_reconnect()
|
||||||
break
|
break
|
||||||
|
@ -469,20 +469,20 @@ class MTProtoSender:
|
||||||
continue
|
continue
|
||||||
except:
|
except:
|
||||||
__log__.exception('Unhandled exception while unpacking')
|
__log__.exception('Unhandled exception while unpacking')
|
||||||
await asyncio.sleep(1)
|
asyncio.sleep(1)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
await self._process_message(message)
|
self._process_message(message)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
return
|
return
|
||||||
except:
|
except:
|
||||||
__log__.exception('Unhandled exception while '
|
__log__.exception('Unhandled exception while '
|
||||||
'processing %s', message)
|
'processing %s', message)
|
||||||
await asyncio.sleep(1)
|
asyncio.sleep(1)
|
||||||
|
|
||||||
# Response Handlers
|
# Response Handlers
|
||||||
|
|
||||||
async def _process_message(self, message):
|
def _process_message(self, message):
|
||||||
"""
|
"""
|
||||||
Adds the given message to the list of messages that must be
|
Adds the given message to the list of messages that must be
|
||||||
acknowledged and dispatches control to different ``_handle_*``
|
acknowledged and dispatches control to different ``_handle_*``
|
||||||
|
@ -491,9 +491,9 @@ class MTProtoSender:
|
||||||
self._pending_ack.add(message.msg_id)
|
self._pending_ack.add(message.msg_id)
|
||||||
handler = self._handlers.get(message.obj.CONSTRUCTOR_ID,
|
handler = self._handlers.get(message.obj.CONSTRUCTOR_ID,
|
||||||
self._handle_update)
|
self._handle_update)
|
||||||
await handler(message)
|
handler(message)
|
||||||
|
|
||||||
async def _handle_rpc_result(self, message):
|
def _handle_rpc_result(self, message):
|
||||||
"""
|
"""
|
||||||
Handles the result for Remote Procedure Calls:
|
Handles the result for Remote Procedure Calls:
|
||||||
|
|
||||||
|
@ -529,7 +529,7 @@ class MTProtoSender:
|
||||||
__log__.info('Received response without parent request: {}'
|
__log__.info('Received response without parent request: {}'
|
||||||
.format(rpc_result.body))
|
.format(rpc_result.body))
|
||||||
|
|
||||||
async def _handle_container(self, message):
|
def _handle_container(self, message):
|
||||||
"""
|
"""
|
||||||
Processes the inner messages of a container with many of them:
|
Processes the inner messages of a container with many of them:
|
||||||
|
|
||||||
|
@ -537,9 +537,9 @@ class MTProtoSender:
|
||||||
"""
|
"""
|
||||||
__log__.debug('Handling container')
|
__log__.debug('Handling container')
|
||||||
for inner_message in message.obj.messages:
|
for inner_message in message.obj.messages:
|
||||||
await self._process_message(inner_message)
|
self._process_message(inner_message)
|
||||||
|
|
||||||
async def _handle_gzip_packed(self, message):
|
def _handle_gzip_packed(self, message):
|
||||||
"""
|
"""
|
||||||
Unpacks the data from a gzipped object and processes it:
|
Unpacks the data from a gzipped object and processes it:
|
||||||
|
|
||||||
|
@ -548,15 +548,15 @@ class MTProtoSender:
|
||||||
__log__.debug('Handling gzipped data')
|
__log__.debug('Handling gzipped data')
|
||||||
with BinaryReader(message.obj.data) as reader:
|
with BinaryReader(message.obj.data) as reader:
|
||||||
message.obj = reader.tgread_object()
|
message.obj = reader.tgread_object()
|
||||||
await self._process_message(message)
|
self._process_message(message)
|
||||||
|
|
||||||
async def _handle_update(self, message):
|
def _handle_update(self, message):
|
||||||
__log__.debug('Handling update {}'
|
__log__.debug('Handling update {}'
|
||||||
.format(message.obj.__class__.__name__))
|
.format(message.obj.__class__.__name__))
|
||||||
if self._update_callback:
|
if self._update_callback:
|
||||||
self._update_callback(message.obj)
|
self._update_callback(message.obj)
|
||||||
|
|
||||||
async def _handle_pong(self, message):
|
def _handle_pong(self, message):
|
||||||
"""
|
"""
|
||||||
Handles pong results, which don't come inside a ``rpc_result``
|
Handles pong results, which don't come inside a ``rpc_result``
|
||||||
but are still sent through a request:
|
but are still sent through a request:
|
||||||
|
@ -569,7 +569,7 @@ class MTProtoSender:
|
||||||
if message:
|
if message:
|
||||||
message.future.set_result(pong)
|
message.future.set_result(pong)
|
||||||
|
|
||||||
async def _handle_bad_server_salt(self, message):
|
def _handle_bad_server_salt(self, message):
|
||||||
"""
|
"""
|
||||||
Corrects the currently used server salt to use the right value
|
Corrects the currently used server salt to use the right value
|
||||||
before enqueuing the rejected message to be re-sent:
|
before enqueuing the rejected message to be re-sent:
|
||||||
|
@ -592,7 +592,7 @@ class MTProtoSender:
|
||||||
__log__.info('Message %d not resent due to bad salt',
|
__log__.info('Message %d not resent due to bad salt',
|
||||||
bad_salt.bad_msg_id)
|
bad_salt.bad_msg_id)
|
||||||
|
|
||||||
async def _handle_bad_notification(self, message):
|
def _handle_bad_notification(self, message):
|
||||||
"""
|
"""
|
||||||
Adjusts the current state to be correct based on the
|
Adjusts the current state to be correct based on the
|
||||||
received bad message notification whenever possible:
|
received bad message notification whenever possible:
|
||||||
|
@ -640,7 +640,7 @@ class MTProtoSender:
|
||||||
__log__.info('Message %d not resent due to bad msg',
|
__log__.info('Message %d not resent due to bad msg',
|
||||||
bad_msg.bad_msg_id)
|
bad_msg.bad_msg_id)
|
||||||
|
|
||||||
async def _handle_detailed_info(self, message):
|
def _handle_detailed_info(self, message):
|
||||||
"""
|
"""
|
||||||
Updates the current status with the received detailed information:
|
Updates the current status with the received detailed information:
|
||||||
|
|
||||||
|
@ -652,7 +652,7 @@ class MTProtoSender:
|
||||||
__log__.debug('Handling detailed info for message %d', msg_id)
|
__log__.debug('Handling detailed info for message %d', msg_id)
|
||||||
self._pending_ack.add(msg_id)
|
self._pending_ack.add(msg_id)
|
||||||
|
|
||||||
async def _handle_new_detailed_info(self, message):
|
def _handle_new_detailed_info(self, message):
|
||||||
"""
|
"""
|
||||||
Updates the current status with the received detailed information:
|
Updates the current status with the received detailed information:
|
||||||
|
|
||||||
|
@ -664,7 +664,7 @@ class MTProtoSender:
|
||||||
__log__.debug('Handling new detailed info for message %d', msg_id)
|
__log__.debug('Handling new detailed info for message %d', msg_id)
|
||||||
self._pending_ack.add(msg_id)
|
self._pending_ack.add(msg_id)
|
||||||
|
|
||||||
async def _handle_new_session_created(self, message):
|
def _handle_new_session_created(self, message):
|
||||||
"""
|
"""
|
||||||
Updates the current status with the received session information:
|
Updates the current status with the received session information:
|
||||||
|
|
||||||
|
@ -675,7 +675,7 @@ class MTProtoSender:
|
||||||
__log__.debug('Handling new session created')
|
__log__.debug('Handling new session created')
|
||||||
self.state.salt = message.obj.server_salt
|
self.state.salt = message.obj.server_salt
|
||||||
|
|
||||||
async def _handle_ack(self, message):
|
def _handle_ack(self, message):
|
||||||
"""
|
"""
|
||||||
Handles a server acknowledge about our messages. Normally
|
Handles a server acknowledge about our messages. Normally
|
||||||
these can be ignored except in the case of ``auth.logOut``:
|
these can be ignored except in the case of ``auth.logOut``:
|
||||||
|
@ -701,7 +701,7 @@ class MTProtoSender:
|
||||||
del self._pending_messages[msg_id]
|
del self._pending_messages[msg_id]
|
||||||
msg.future.set_result(True)
|
msg.future.set_result(True)
|
||||||
|
|
||||||
async def _handle_future_salts(self, message):
|
def _handle_future_salts(self, message):
|
||||||
"""
|
"""
|
||||||
Handles future salt results, which don't come inside a
|
Handles future salt results, which don't come inside a
|
||||||
``rpc_result`` but are still sent through a request:
|
``rpc_result`` but are still sent through a request:
|
||||||
|
@ -716,7 +716,7 @@ class MTProtoSender:
|
||||||
if msg:
|
if msg:
|
||||||
msg.future.set_result(message.obj)
|
msg.future.set_result(message.obj)
|
||||||
|
|
||||||
async def _handle_state_forgotten(self, message):
|
def _handle_state_forgotten(self, message):
|
||||||
"""
|
"""
|
||||||
Handles both :tl:`MsgsStateReq` and :tl:`MsgResendReq` by
|
Handles both :tl:`MsgsStateReq` and :tl:`MsgResendReq` by
|
||||||
enqueuing a :tl:`MsgsStateInfo` to be sent at a later point.
|
enqueuing a :tl:`MsgsStateInfo` to be sent at a later point.
|
||||||
|
@ -724,7 +724,7 @@ class MTProtoSender:
|
||||||
self.send(MsgsStateInfo(req_msg_id=message.msg_id,
|
self.send(MsgsStateInfo(req_msg_id=message.msg_id,
|
||||||
info=chr(1) * len(message.obj.msg_ids)))
|
info=chr(1) * len(message.obj.msg_ids)))
|
||||||
|
|
||||||
async def _handle_msg_all(self, message):
|
def _handle_msg_all(self, message):
|
||||||
"""
|
"""
|
||||||
Handles :tl:`MsgsAllInfo` by doing nothing (yet).
|
Handles :tl:`MsgsAllInfo` by doing nothing (yet).
|
||||||
"""
|
"""
|
||||||
|
@ -741,8 +741,8 @@ class _ContainerQueue(asyncio.Queue):
|
||||||
``asyncio.Queue`` when needed for testing purposes, and
|
``asyncio.Queue`` when needed for testing purposes, and
|
||||||
a list won't be returned in said case.
|
a list won't be returned in said case.
|
||||||
"""
|
"""
|
||||||
async def get(self):
|
def get(self):
|
||||||
result = await super().get()
|
result = super().get()
|
||||||
if self.empty() or result == _reconnect_sentinel or\
|
if self.empty() or result == _reconnect_sentinel or\
|
||||||
isinstance(result.obj, MessageContainer):
|
isinstance(result.obj, MessageContainer):
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -1,81 +1,2 @@
|
||||||
"""
|
|
||||||
This magical module will rewrite all public methods in the public interface
|
|
||||||
of the library so they can run the loop on their own if it's not already
|
|
||||||
running. This rewrite may not be desirable if the end user always uses the
|
|
||||||
methods they way they should be ran, but it's incredibly useful for quick
|
|
||||||
scripts and the runtime overhead is relatively low.
|
|
||||||
|
|
||||||
Some really common methods which are hardly used offer this ability by
|
|
||||||
default, such as ``.start()`` and ``.run_until_disconnected()`` (since
|
|
||||||
you may want to start, and then run until disconnected while using async
|
|
||||||
event handlers).
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
import functools
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
from async_generator import isasyncgenfunction
|
|
||||||
|
|
||||||
from .client.telegramclient import TelegramClient
|
|
||||||
from .tl.custom import Draft, Dialog, MessageButton, Forward, Message
|
|
||||||
|
|
||||||
|
|
||||||
def _syncify_coro(t, method_name):
|
|
||||||
method = getattr(t, method_name)
|
|
||||||
|
|
||||||
@functools.wraps(method)
|
|
||||||
def syncified(*args, **kwargs):
|
|
||||||
coro = method(*args, **kwargs)
|
|
||||||
return (
|
|
||||||
coro if asyncio.get_event_loop().is_running()
|
|
||||||
else asyncio.get_event_loop().run_until_complete(coro)
|
|
||||||
)
|
|
||||||
|
|
||||||
setattr(t, method_name, syncified)
|
|
||||||
|
|
||||||
|
|
||||||
class _SyncGen:
|
|
||||||
def __init__(self, loop, gen):
|
|
||||||
self.loop = loop
|
|
||||||
self.gen = gen
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __next__(self):
|
|
||||||
try:
|
|
||||||
return self.loop.run_until_complete(self.gen.__anext__())
|
|
||||||
except StopAsyncIteration:
|
|
||||||
raise StopIteration from None
|
|
||||||
|
|
||||||
|
|
||||||
def _syncify_gen(t, method_name):
|
|
||||||
method = getattr(t, method_name)
|
|
||||||
|
|
||||||
@functools.wraps(method)
|
|
||||||
def syncified(*args, **kwargs):
|
|
||||||
coro = method(*args, **kwargs)
|
|
||||||
return (
|
|
||||||
coro if asyncio.get_event_loop().is_running()
|
|
||||||
else _SyncGen(asyncio.get_event_loop(), coro)
|
|
||||||
)
|
|
||||||
|
|
||||||
setattr(t, method_name, syncified)
|
|
||||||
|
|
||||||
|
|
||||||
def syncify(*types):
|
def syncify(*types):
|
||||||
"""
|
pass
|
||||||
Converts all the methods in the given types (class definitions)
|
|
||||||
into synchronous, which return either the coroutine or the result
|
|
||||||
based on whether ``asyncio's`` event loop is running.
|
|
||||||
"""
|
|
||||||
for t in types:
|
|
||||||
for method_name in dir(t):
|
|
||||||
if not method_name.startswith('_') or method_name == '__call__':
|
|
||||||
if inspect.iscoroutinefunction(getattr(t, method_name)):
|
|
||||||
_syncify_coro(t, method_name)
|
|
||||||
elif isasyncgenfunction(getattr(t, method_name)):
|
|
||||||
_syncify_gen(t, method_name)
|
|
||||||
|
|
||||||
|
|
||||||
syncify(TelegramClient, Draft, Dialog, MessageButton, Forward, Message)
|
|
||||||
|
|
|
@ -88,12 +88,12 @@ class Dialog:
|
||||||
)
|
)
|
||||||
self.is_channel = isinstance(self.entity, types.Channel)
|
self.is_channel = isinstance(self.entity, types.Channel)
|
||||||
|
|
||||||
async def send_message(self, *args, **kwargs):
|
def send_message(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Sends a message to this dialog. This is just a wrapper around
|
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 await self._client.send_message(
|
return self._client.send_message(
|
||||||
self.input_entity, *args, **kwargs)
|
self.input_entity, *args, **kwargs)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
|
|
@ -71,11 +71,11 @@ class Draft:
|
||||||
|
|
||||||
return self._input_entity
|
return self._input_entity
|
||||||
|
|
||||||
async def get_entity(self):
|
def get_entity(self):
|
||||||
"""
|
"""
|
||||||
Returns `entity` but will make an API call if necessary.
|
Returns `entity` but will make an API call if necessary.
|
||||||
"""
|
"""
|
||||||
if not self.entity and await self.get_input_entity():
|
if not self.entity and self.get_input_entity():
|
||||||
try:
|
try:
|
||||||
self._entity =\
|
self._entity =\
|
||||||
self._client.get_entity(self._input_entity)
|
self._client.get_entity(self._input_entity)
|
||||||
|
@ -84,7 +84,7 @@ class Draft:
|
||||||
|
|
||||||
return self._entity
|
return self._entity
|
||||||
|
|
||||||
async def get_input_entity(self):
|
def get_input_entity(self):
|
||||||
"""
|
"""
|
||||||
Returns `input_entity` but will make an API call if necessary.
|
Returns `input_entity` but will make an API call if necessary.
|
||||||
"""
|
"""
|
||||||
|
@ -115,7 +115,7 @@ class Draft:
|
||||||
"""
|
"""
|
||||||
return not self._text
|
return not self._text
|
||||||
|
|
||||||
async def set_message(
|
def set_message(
|
||||||
self, text=None, reply_to=0, parse_mode=Default,
|
self, text=None, reply_to=0, parse_mode=Default,
|
||||||
link_preview=None):
|
link_preview=None):
|
||||||
"""
|
"""
|
||||||
|
@ -144,9 +144,9 @@ class Draft:
|
||||||
link_preview = self.link_preview
|
link_preview = self.link_preview
|
||||||
|
|
||||||
raw_text, entities =\
|
raw_text, entities =\
|
||||||
await self._client._parse_message_text(text, parse_mode)
|
self._client._parse_message_text(text, parse_mode)
|
||||||
|
|
||||||
result = await self._client(SaveDraftRequest(
|
result = self._client(SaveDraftRequest(
|
||||||
peer=self._peer,
|
peer=self._peer,
|
||||||
message=raw_text,
|
message=raw_text,
|
||||||
no_webpage=not link_preview,
|
no_webpage=not link_preview,
|
||||||
|
@ -163,22 +163,22 @@ class Draft:
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def send(self, clear=True, parse_mode=Default):
|
def send(self, clear=True, parse_mode=Default):
|
||||||
"""
|
"""
|
||||||
Sends the contents of this draft to the dialog. This is just a
|
Sends the contents of this draft to the dialog. This is just a
|
||||||
wrapper around ``send_message(dialog.input_entity, *args, **kwargs)``.
|
wrapper around ``send_message(dialog.input_entity, *args, **kwargs)``.
|
||||||
"""
|
"""
|
||||||
await self._client.send_message(
|
self._client.send_message(
|
||||||
self._peer, self.text, reply_to=self.reply_to_msg_id,
|
self._peer, self.text, reply_to=self.reply_to_msg_id,
|
||||||
link_preview=self.link_preview, parse_mode=parse_mode,
|
link_preview=self.link_preview, parse_mode=parse_mode,
|
||||||
clear_draft=clear
|
clear_draft=clear
|
||||||
)
|
)
|
||||||
|
|
||||||
async def delete(self):
|
def delete(self):
|
||||||
"""
|
"""
|
||||||
Deletes this draft, and returns ``True`` on success.
|
Deletes this draft, and returns ``True`` on success.
|
||||||
"""
|
"""
|
||||||
return await self.set_message(text='')
|
return self.set_message(text='')
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -43,14 +43,14 @@ class Forward:
|
||||||
"""
|
"""
|
||||||
return self._sender
|
return self._sender
|
||||||
|
|
||||||
async def get_sender(self):
|
def get_sender(self):
|
||||||
"""
|
"""
|
||||||
Returns `sender` but will make an API if necessary.
|
Returns `sender` but will make an API if necessary.
|
||||||
"""
|
"""
|
||||||
if not self.sender and self.original_fwd.from_id:
|
if not self.sender and self.original_fwd.from_id:
|
||||||
try:
|
try:
|
||||||
self._sender = await self._client.get_entity(
|
self._sender = self._client.get_entity(
|
||||||
await self.get_input_sender())
|
self.get_input_sender())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# TODO We could reload the message
|
# TODO We could reload the message
|
||||||
pass
|
pass
|
||||||
|
@ -71,7 +71,7 @@ class Forward:
|
||||||
|
|
||||||
return self._input_sender
|
return self._input_sender
|
||||||
|
|
||||||
async def get_input_sender(self):
|
def get_input_sender(self):
|
||||||
"""
|
"""
|
||||||
Returns `input_sender` but will make an API call if necessary.
|
Returns `input_sender` but will make an API call if necessary.
|
||||||
"""
|
"""
|
||||||
|
@ -87,14 +87,14 @@ class Forward:
|
||||||
"""
|
"""
|
||||||
return self._chat
|
return self._chat
|
||||||
|
|
||||||
async def get_chat(self):
|
def get_chat(self):
|
||||||
"""
|
"""
|
||||||
Returns `chat` but will make an API if necessary.
|
Returns `chat` but will make an API if necessary.
|
||||||
"""
|
"""
|
||||||
if not self.chat and self.original_fwd.channel_id:
|
if not self.chat and self.original_fwd.channel_id:
|
||||||
try:
|
try:
|
||||||
self._chat = await self._client.get_entity(
|
self._chat = self._client.get_entity(
|
||||||
await self.get_input_chat())
|
self.get_input_chat())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# TODO We could reload the message
|
# TODO We could reload the message
|
||||||
pass
|
pass
|
||||||
|
@ -115,7 +115,7 @@ class Forward:
|
||||||
|
|
||||||
return self._input_chat
|
return self._input_chat
|
||||||
|
|
||||||
async def get_input_chat(self):
|
def get_input_chat(self):
|
||||||
"""
|
"""
|
||||||
Returns `input_chat` but will make an API call if necessary.
|
Returns `input_chat` but will make an API call if necessary.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -147,14 +147,14 @@ class Message:
|
||||||
return self.original_message.action
|
return self.original_message.action
|
||||||
|
|
||||||
# TODO Make a property for via_bot and via_input_bot, as well as get_*
|
# TODO Make a property for via_bot and via_input_bot, as well as get_*
|
||||||
async def _reload_message(self):
|
def _reload_message(self):
|
||||||
"""
|
"""
|
||||||
Re-fetches this message to reload the sender and chat entities,
|
Re-fetches this message to reload the sender and chat entities,
|
||||||
along with their input versions.
|
along with their input versions.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
chat = await self.get_input_chat() if self.is_channel else None
|
chat = self.get_input_chat() if self.is_channel else None
|
||||||
msg = await self._client.get_messages(
|
msg = self._client.get_messages(
|
||||||
chat, ids=self.original_message.id)
|
chat, ids=self.original_message.id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return # We may not have the input chat/get message failed
|
return # We may not have the input chat/get message failed
|
||||||
|
@ -177,17 +177,17 @@ class Message:
|
||||||
"""
|
"""
|
||||||
return self._sender
|
return self._sender
|
||||||
|
|
||||||
async def get_sender(self):
|
def get_sender(self):
|
||||||
"""
|
"""
|
||||||
Returns `sender`, but will make an API call to find the
|
Returns `sender`, but will make an API call to find the
|
||||||
sender unless it's already cached.
|
sender unless it's already cached.
|
||||||
"""
|
"""
|
||||||
if self._sender is None and await self.get_input_sender():
|
if self._sender is None and self.get_input_sender():
|
||||||
try:
|
try:
|
||||||
self._sender =\
|
self._sender =\
|
||||||
await self._client.get_entity(self._input_sender)
|
self._client.get_entity(self._input_sender)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await self._reload_message()
|
self._reload_message()
|
||||||
return self._sender
|
return self._sender
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -201,17 +201,17 @@ class Message:
|
||||||
"""
|
"""
|
||||||
return self._chat
|
return self._chat
|
||||||
|
|
||||||
async def get_chat(self):
|
def get_chat(self):
|
||||||
"""
|
"""
|
||||||
Returns `chat`, but will make an API call to find the
|
Returns `chat`, but will make an API call to find the
|
||||||
chat unless it's already cached.
|
chat unless it's already cached.
|
||||||
"""
|
"""
|
||||||
if self._chat is None and await self.get_input_chat():
|
if self._chat is None and self.get_input_chat():
|
||||||
try:
|
try:
|
||||||
self._chat =\
|
self._chat =\
|
||||||
await self._client.get_entity(self._input_chat)
|
self._client.get_entity(self._input_chat)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await self._reload_message()
|
self._reload_message()
|
||||||
return self._chat
|
return self._chat
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -234,14 +234,14 @@ class Message:
|
||||||
pass
|
pass
|
||||||
return self._input_sender
|
return self._input_sender
|
||||||
|
|
||||||
async def get_input_sender(self):
|
def get_input_sender(self):
|
||||||
"""
|
"""
|
||||||
Returns `input_sender`, but will make an API call to find the
|
Returns `input_sender`, but will make an API call to find the
|
||||||
input sender unless it's already cached.
|
input sender unless it's already cached.
|
||||||
"""
|
"""
|
||||||
if self.input_sender is None\
|
if self.input_sender is None\
|
||||||
and not self.is_channel and not self.is_group:
|
and not self.is_channel and not self.is_group:
|
||||||
await self._reload_message()
|
self._reload_message()
|
||||||
return self._input_sender
|
return self._input_sender
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -263,7 +263,7 @@ class Message:
|
||||||
|
|
||||||
return self._input_chat
|
return self._input_chat
|
||||||
|
|
||||||
async def get_input_chat(self):
|
def get_input_chat(self):
|
||||||
"""
|
"""
|
||||||
Returns `input_chat`, but will make an API call to find the
|
Returns `input_chat`, but will make an API call to find the
|
||||||
input chat unless it's already cached.
|
input chat unless it's already cached.
|
||||||
|
@ -273,7 +273,7 @@ class Message:
|
||||||
# The input chat cannot rely on ._reload_message() because
|
# The input chat cannot rely on ._reload_message() because
|
||||||
# said method may need the input chat.
|
# said method may need the input chat.
|
||||||
target = self.chat_id
|
target = self.chat_id
|
||||||
async for d in self._client.iter_dialogs(100):
|
for d in self._client.iter_dialogs(100):
|
||||||
if d.id == target:
|
if d.id == target:
|
||||||
self._chat = d.entity
|
self._chat = d.entity
|
||||||
self._input_chat = d.input_entity
|
self._input_chat = d.input_entity
|
||||||
|
@ -385,20 +385,20 @@ class Message:
|
||||||
|
|
||||||
return self._buttons
|
return self._buttons
|
||||||
|
|
||||||
async def get_buttons(self):
|
def get_buttons(self):
|
||||||
"""
|
"""
|
||||||
Returns `buttons`, but will make an API call to find the
|
Returns `buttons`, but will make an API call to find the
|
||||||
input chat (needed for the buttons) unless it's already cached.
|
input chat (needed for the buttons) unless it's already cached.
|
||||||
"""
|
"""
|
||||||
if not self.buttons and isinstance(
|
if not self.buttons and isinstance(
|
||||||
self.original_message, types.Message):
|
self.original_message, types.Message):
|
||||||
chat = await self.get_input_chat()
|
chat = self.get_input_chat()
|
||||||
if not chat:
|
if not chat:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
bot = self._needed_markup_bot()
|
bot = self._needed_markup_bot()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await self._reload_message()
|
self._reload_message()
|
||||||
bot = self._needed_markup_bot() # TODO use via_input_bot
|
bot = self._needed_markup_bot() # TODO use via_input_bot
|
||||||
|
|
||||||
self._set_buttons(chat, bot)
|
self._set_buttons(chat, bot)
|
||||||
|
@ -521,7 +521,7 @@ class Message:
|
||||||
"""
|
"""
|
||||||
return self.original_message.out
|
return self.original_message.out
|
||||||
|
|
||||||
async def get_reply_message(self):
|
def get_reply_message(self):
|
||||||
"""
|
"""
|
||||||
The `telethon.tl.custom.message.Message` that this message is replying
|
The `telethon.tl.custom.message.Message` that this message is replying
|
||||||
to, or ``None``.
|
to, or ``None``.
|
||||||
|
@ -532,33 +532,33 @@ class Message:
|
||||||
if self._reply_message is None:
|
if self._reply_message is None:
|
||||||
if not self.original_message.reply_to_msg_id:
|
if not self.original_message.reply_to_msg_id:
|
||||||
return None
|
return None
|
||||||
self._reply_message = await self._client.get_messages(
|
self._reply_message = self._client.get_messages(
|
||||||
await self.get_input_chat() if self.is_channel else None,
|
self.get_input_chat() if self.is_channel else None,
|
||||||
ids=self.original_message.reply_to_msg_id
|
ids=self.original_message.reply_to_msg_id
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._reply_message
|
return self._reply_message
|
||||||
|
|
||||||
async def respond(self, *args, **kwargs):
|
def respond(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Responds to the message (not as a reply). Shorthand for
|
Responds to the message (not as a reply). Shorthand for
|
||||||
`telethon.telegram_client.TelegramClient.send_message` with
|
`telethon.telegram_client.TelegramClient.send_message` with
|
||||||
``entity`` already set.
|
``entity`` already set.
|
||||||
"""
|
"""
|
||||||
return await self._client.send_message(
|
return self._client.send_message(
|
||||||
await self.get_input_chat(), *args, **kwargs)
|
self.get_input_chat(), *args, **kwargs)
|
||||||
|
|
||||||
async def reply(self, *args, **kwargs):
|
def reply(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Replies to the message (as a reply). Shorthand for
|
Replies to the message (as a reply). Shorthand for
|
||||||
`telethon.telegram_client.TelegramClient.send_message` with
|
`telethon.telegram_client.TelegramClient.send_message` with
|
||||||
both ``entity`` and ``reply_to`` already set.
|
both ``entity`` and ``reply_to`` already set.
|
||||||
"""
|
"""
|
||||||
kwargs['reply_to'] = self.original_message.id
|
kwargs['reply_to'] = self.original_message.id
|
||||||
return await self._client.send_message(
|
return self._client.send_message(
|
||||||
await self.get_input_chat(), *args, **kwargs)
|
self.get_input_chat(), *args, **kwargs)
|
||||||
|
|
||||||
async def forward_to(self, *args, **kwargs):
|
def forward_to(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Forwards the message. Shorthand for
|
Forwards the message. Shorthand for
|
||||||
`telethon.telegram_client.TelegramClient.forward_messages` with
|
`telethon.telegram_client.TelegramClient.forward_messages` with
|
||||||
|
@ -569,10 +569,10 @@ class Message:
|
||||||
`telethon.telegram_client.TelegramClient` instance directly.
|
`telethon.telegram_client.TelegramClient` instance directly.
|
||||||
"""
|
"""
|
||||||
kwargs['messages'] = self.original_message.id
|
kwargs['messages'] = self.original_message.id
|
||||||
kwargs['from_peer'] = await self.get_input_chat()
|
kwargs['from_peer'] = self.get_input_chat()
|
||||||
return await self._client.forward_messages(*args, **kwargs)
|
return self._client.forward_messages(*args, **kwargs)
|
||||||
|
|
||||||
async def edit(self, *args, **kwargs):
|
def edit(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Edits the message iff it's outgoing. Shorthand for
|
Edits the message iff it's outgoing. Shorthand for
|
||||||
`telethon.telegram_client.TelegramClient.edit_message` with
|
`telethon.telegram_client.TelegramClient.edit_message` with
|
||||||
|
@ -590,12 +590,12 @@ class Message:
|
||||||
if self.original_message.to_id.user_id != me.user_id:
|
if self.original_message.to_id.user_id != me.user_id:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return await self._client.edit_message(
|
return self._client.edit_message(
|
||||||
await self.get_input_chat(), self.original_message,
|
self.get_input_chat(), self.original_message,
|
||||||
*args, **kwargs
|
*args, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
async def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Deletes the message. You're responsible for checking whether you
|
Deletes the message. You're responsible for checking whether you
|
||||||
have the permission to do so, or to except the error otherwise.
|
have the permission to do so, or to except the error otherwise.
|
||||||
|
@ -607,18 +607,18 @@ class Message:
|
||||||
this `delete` method. Use a
|
this `delete` method. Use a
|
||||||
`telethon.telegram_client.TelegramClient` instance directly.
|
`telethon.telegram_client.TelegramClient` instance directly.
|
||||||
"""
|
"""
|
||||||
return await self._client.delete_messages(
|
return self._client.delete_messages(
|
||||||
await self.get_input_chat(), [self.original_message],
|
self.get_input_chat(), [self.original_message],
|
||||||
*args, **kwargs
|
*args, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
async def download_media(self, *args, **kwargs):
|
def download_media(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Downloads the media contained in the message, if any.
|
Downloads the media contained in the message, if any.
|
||||||
`telethon.telegram_client.TelegramClient.download_media` with
|
`telethon.telegram_client.TelegramClient.download_media` with
|
||||||
the ``message`` already set.
|
the ``message`` already set.
|
||||||
"""
|
"""
|
||||||
return await self._client.download_media(
|
return self._client.download_media(
|
||||||
self.original_message, *args, **kwargs)
|
self.original_message, *args, **kwargs)
|
||||||
|
|
||||||
def get_entities_text(self, cls=None):
|
def get_entities_text(self, cls=None):
|
||||||
|
@ -647,7 +647,7 @@ class Message:
|
||||||
texts = get_inner_text(self.original_message.message, ent)
|
texts = get_inner_text(self.original_message.message, ent)
|
||||||
return list(zip(ent, texts))
|
return list(zip(ent, texts))
|
||||||
|
|
||||||
async def click(self, i=None, j=None, *, text=None, filter=None):
|
def click(self, i=None, j=None, *, text=None, filter=None):
|
||||||
"""
|
"""
|
||||||
Calls `telethon.tl.custom.messagebutton.MessageButton.click`
|
Calls `telethon.tl.custom.messagebutton.MessageButton.click`
|
||||||
for the specified button.
|
for the specified button.
|
||||||
|
@ -691,32 +691,32 @@ class Message:
|
||||||
if sum(int(x is not None) for x in (i, text, filter)) >= 2:
|
if sum(int(x is not None) for x in (i, text, filter)) >= 2:
|
||||||
raise ValueError('You can only set either of i, text or filter')
|
raise ValueError('You can only set either of i, text or filter')
|
||||||
|
|
||||||
if not await self.get_buttons():
|
if not self.get_buttons():
|
||||||
return # Accessing the property sets self._buttons[_flat]
|
return # Accessing the property sets self._buttons[_flat]
|
||||||
|
|
||||||
if text is not None:
|
if text is not None:
|
||||||
if callable(text):
|
if callable(text):
|
||||||
for button in self._buttons_flat:
|
for button in self._buttons_flat:
|
||||||
if text(button.text):
|
if text(button.text):
|
||||||
return await button.click()
|
return button.click()
|
||||||
else:
|
else:
|
||||||
for button in self._buttons_flat:
|
for button in self._buttons_flat:
|
||||||
if button.text == text:
|
if button.text == text:
|
||||||
return await button.click()
|
return button.click()
|
||||||
return
|
return
|
||||||
|
|
||||||
if filter is not None:
|
if filter is not None:
|
||||||
for button in self._buttons_flat:
|
for button in self._buttons_flat:
|
||||||
if filter(button):
|
if filter(button):
|
||||||
return await button.click()
|
return button.click()
|
||||||
return
|
return
|
||||||
|
|
||||||
if i is None:
|
if i is None:
|
||||||
i = 0
|
i = 0
|
||||||
if j is None:
|
if j is None:
|
||||||
return await self._buttons_flat[i].click()
|
return self._buttons_flat[i].click()
|
||||||
else:
|
else:
|
||||||
return await self._buttons[i][j].click()
|
return self._buttons[i][j].click()
|
||||||
|
|
||||||
|
|
||||||
class _CustomMessage(Message, types.Message):
|
class _CustomMessage(Message, types.Message):
|
||||||
|
|
|
@ -52,7 +52,7 @@ class MessageButton:
|
||||||
if isinstance(self.button, types.KeyboardButtonUrl):
|
if isinstance(self.button, types.KeyboardButtonUrl):
|
||||||
return self.button.url
|
return self.button.url
|
||||||
|
|
||||||
async def click(self):
|
def click(self):
|
||||||
"""
|
"""
|
||||||
Emulates the behaviour of clicking this button.
|
Emulates the behaviour of clicking this button.
|
||||||
|
|
||||||
|
@ -70,18 +70,18 @@ class MessageButton:
|
||||||
be passed to ``webbrowser.open`` and return ``True`` on success.
|
be passed to ``webbrowser.open`` and return ``True`` on success.
|
||||||
"""
|
"""
|
||||||
if isinstance(self.button, types.KeyboardButton):
|
if isinstance(self.button, types.KeyboardButton):
|
||||||
return await self._client.send_message(
|
return self._client.send_message(
|
||||||
self._chat, self.button.text, reply_to=self._msg_id)
|
self._chat, self.button.text, reply_to=self._msg_id)
|
||||||
elif isinstance(self.button, types.KeyboardButtonCallback):
|
elif isinstance(self.button, types.KeyboardButtonCallback):
|
||||||
req = functions.messages.GetBotCallbackAnswerRequest(
|
req = functions.messages.GetBotCallbackAnswerRequest(
|
||||||
peer=self._chat, msg_id=self._msg_id, data=self.button.data
|
peer=self._chat, msg_id=self._msg_id, data=self.button.data
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return await self._client(req)
|
return self._client(req)
|
||||||
except BotTimeout:
|
except BotTimeout:
|
||||||
return None
|
return None
|
||||||
elif isinstance(self.button, types.KeyboardButtonSwitchInline):
|
elif isinstance(self.button, types.KeyboardButtonSwitchInline):
|
||||||
return await self._client(functions.messages.StartBotRequest(
|
return self._client(functions.messages.StartBotRequest(
|
||||||
bot=self._bot, peer=self._chat, start_param=self.button.query
|
bot=self._bot, peer=self._chat, start_param=self.button.query
|
||||||
))
|
))
|
||||||
elif isinstance(self.button, types.KeyboardButtonUrl):
|
elif isinstance(self.button, types.KeyboardButtonUrl):
|
||||||
|
|
|
@ -162,5 +162,5 @@ class TLRequest(TLObject):
|
||||||
def read_result(reader):
|
def read_result(reader):
|
||||||
return reader.tgread_object()
|
return reader.tgread_object()
|
||||||
|
|
||||||
async def resolve(self, client, utils):
|
def resolve(self, client, utils):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,218 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import difflib
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
from telethon import TelegramClient, events, custom
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.WARNING)
|
|
||||||
logging.getLogger('asyncio').setLevel(logging.ERROR)
|
|
||||||
|
|
||||||
for x in 'TG_API_ID TG_API_HASH TG_TOKEN'.split():
|
|
||||||
if x not in os.environ:
|
|
||||||
print(f'{x} not in environmental variables', file=sys.stderr)
|
|
||||||
quit()
|
|
||||||
|
|
||||||
NAME = os.environ['TG_TOKEN'].split(':')[0]
|
|
||||||
bot = TelegramClient(NAME, os.environ['TG_API_ID'], os.environ['TG_API_HASH'])
|
|
||||||
|
|
||||||
|
|
||||||
# ============================== Constants ==============================
|
|
||||||
WELCOME = (
|
|
||||||
'Hi and welcome to the group. Before asking any questions, **please** '
|
|
||||||
'read [the docs](https://telethon.readthedocs.io/). Make sure you are '
|
|
||||||
'using the latest version with `pip3 install -U telethon`, since most '
|
|
||||||
'problems have already been fixed in newer versions.'
|
|
||||||
)
|
|
||||||
|
|
||||||
READ_FULL = (
|
|
||||||
'Please read [Accessing the Full API](https://telethon.readthedocs.io'
|
|
||||||
'/en/latest/extra/advanced-usage/accessing-the-full-api.html)'
|
|
||||||
)
|
|
||||||
|
|
||||||
SEARCH = (
|
|
||||||
'Remember [search is your friend]'
|
|
||||||
'(https://lonamiwebs.github.io/Telethon/?q={})'
|
|
||||||
)
|
|
||||||
|
|
||||||
DOCS = 'TL Reference for [{}](https://lonamiwebs.github.io/Telethon/?q={})'
|
|
||||||
RTD = '[Read The Docs!](https://telethon.readthedocs.io)'
|
|
||||||
RTFD = '[Read The F* Docs!](https://telethon.readthedocs.io)'
|
|
||||||
DOCS_CLIENT = 'https://telethon.readthedocs.io/en/latest/telethon.client.html#'
|
|
||||||
DOCS_MESSAGE = (
|
|
||||||
'https://telethon.readthedocs.io/en/latest/'
|
|
||||||
'telethon.tl.custom.html#telethon.tl.custom.message.Message.'
|
|
||||||
)
|
|
||||||
# ============================== Constants ==============================
|
|
||||||
# ============================== Welcome ==============================
|
|
||||||
last_welcome = None
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on(events.ChatAction)
|
|
||||||
async def handler(event):
|
|
||||||
if event.user_joined:
|
|
||||||
global last_welcome
|
|
||||||
if last_welcome is not None:
|
|
||||||
await last_welcome.delete()
|
|
||||||
|
|
||||||
last_welcome = await event.reply(WELCOME)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================== Welcome ==============================
|
|
||||||
# ============================== Commands ==============================
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on(events.NewMessage(pattern='#ping', forwards=False))
|
|
||||||
async def handler(event):
|
|
||||||
s = time.time()
|
|
||||||
message = await event.reply('Pong!')
|
|
||||||
d = time.time() - s
|
|
||||||
await message.edit(f'Pong! __(reply took {d:.2f}s)__')
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
await asyncio.wait([event.delete(), message.delete()])
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on(events.NewMessage(pattern='#full', forwards=False))
|
|
||||||
async def handler(event):
|
|
||||||
"""#full: Advises to read "Accessing the full API" in the docs."""
|
|
||||||
await asyncio.wait([
|
|
||||||
event.delete(),
|
|
||||||
event.respond(READ_FULL, reply_to=event.reply_to_msg_id)
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on(events.NewMessage(pattern='#search (.+)', forwards=False))
|
|
||||||
async def handler(event):
|
|
||||||
"""#search query: Searches for "query" in the method reference."""
|
|
||||||
query = urllib.parse.quote(event.pattern_match.group(1))
|
|
||||||
await asyncio.wait([
|
|
||||||
event.delete(),
|
|
||||||
event.respond(SEARCH.format(query), reply_to=event.reply_to_msg_id)
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on(events.NewMessage(pattern='(?i)#(?:docs|ref) (.+)', forwards=False))
|
|
||||||
async def handler(event):
|
|
||||||
"""#docs or #ref query: Like #search but shows the query."""
|
|
||||||
q1 = event.pattern_match.group(1)
|
|
||||||
q2 = urllib.parse.quote(q1)
|
|
||||||
await asyncio.wait([
|
|
||||||
event.delete(),
|
|
||||||
event.respond(DOCS.format(q1, q2), reply_to=event.reply_to_msg_id)
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on(events.NewMessage(pattern='#rt(f)?d', forwards=False))
|
|
||||||
async def handler(event):
|
|
||||||
"""#rtd: Tells the user to please read the docs."""
|
|
||||||
rtd = RTFD if event.pattern_match.group(1) else RTD
|
|
||||||
await asyncio.wait([
|
|
||||||
event.delete(),
|
|
||||||
event.respond(rtd, reply_to=event.reply_to_msg_id)
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on(events.NewMessage(pattern='(?i)#(client|msg) (.+)', forwards=False))
|
|
||||||
async def handler(event):
|
|
||||||
"""#client or #msg query: Looks for the given attribute in RTD."""
|
|
||||||
await event.delete()
|
|
||||||
query = event.pattern_match.group(2).lower()
|
|
||||||
cls = ({'client': TelegramClient, 'msg': custom.Message}
|
|
||||||
[event.pattern_match.group(1)])
|
|
||||||
|
|
||||||
attr = search_attr(cls, query)
|
|
||||||
if not attr:
|
|
||||||
await event.respond(f'No such method "{query}" :/')
|
|
||||||
return
|
|
||||||
|
|
||||||
name = attr
|
|
||||||
if event.pattern_match.group(1) == 'client':
|
|
||||||
attr = attr_fullname(cls, attr)
|
|
||||||
url = DOCS_CLIENT
|
|
||||||
elif event.pattern_match.group(1) == 'msg':
|
|
||||||
name = f'Message.{name}'
|
|
||||||
url = DOCS_MESSAGE
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
await event.respond(
|
|
||||||
f'Documentation for [{name}]({url}{attr})',
|
|
||||||
reply_to=event.reply_to_msg_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on(events.NewMessage(pattern='#list', forwards=False))
|
|
||||||
async def handler(event):
|
|
||||||
await event.delete()
|
|
||||||
text = 'Available commands:\n'
|
|
||||||
for callback, handler in bot.list_event_handlers():
|
|
||||||
if isinstance(handler, events.NewMessage) and callback.__doc__:
|
|
||||||
text += f'\n{callback.__doc__}'
|
|
||||||
|
|
||||||
message = await event.respond(text)
|
|
||||||
await asyncio.sleep(1 * text.count(' ')) # Sleep ~1 second per word
|
|
||||||
await message.delete()
|
|
||||||
|
|
||||||
|
|
||||||
# ============================== Commands ==============================
|
|
||||||
# ============================== AutoReply ==============================
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on(events.NewMessage(pattern='(?i)how (.+?)[\W]*$', forwards=False))
|
|
||||||
@bot.on(events.NewMessage(pattern='(.+?)[\W]*?\?+', forwards=False))
|
|
||||||
async def handler(event):
|
|
||||||
words = event.pattern_match.group(1).split()
|
|
||||||
rates = [
|
|
||||||
search_attr(TelegramClient, ' '.join(words[-i:]), threshold=None)
|
|
||||||
for i in range(1, 4)
|
|
||||||
]
|
|
||||||
what = max(rates, key=lambda t: t[1])
|
|
||||||
if what[1] < 0.7:
|
|
||||||
return
|
|
||||||
|
|
||||||
name = what[0]
|
|
||||||
attr = attr_fullname(TelegramClient, name)
|
|
||||||
await event.reply(
|
|
||||||
f'Documentation for [{name}]({DOCS_CLIENT}{attr})',
|
|
||||||
reply_to=event.reply_to_msg_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# We have two @client.on, both could fire, stop stop that
|
|
||||||
raise events.StopPropagation
|
|
||||||
|
|
||||||
|
|
||||||
# ============================== AutoReply ==============================
|
|
||||||
# ============================== Helpers ==============================
|
|
||||||
|
|
||||||
|
|
||||||
def search_attr(cls, query, threshold=0.6):
|
|
||||||
seq = difflib.SequenceMatcher(b=query, autojunk=False)
|
|
||||||
scores = []
|
|
||||||
for n in dir(cls):
|
|
||||||
if not n.startswith('_'):
|
|
||||||
seq.set_seq1(n)
|
|
||||||
scores.append((n, seq.ratio()))
|
|
||||||
|
|
||||||
scores.sort(key=lambda t: t[1], reverse=True)
|
|
||||||
if threshold is None:
|
|
||||||
return scores[0]
|
|
||||||
else:
|
|
||||||
return scores[0][0] if scores[0][1] >= threshold else None
|
|
||||||
|
|
||||||
|
|
||||||
def attr_fullname(cls, n):
|
|
||||||
m = getattr(cls, n)
|
|
||||||
cls = sys.modules.get(m.__module__)
|
|
||||||
for name in m.__qualname__.split('.')[:-1]:
|
|
||||||
cls = getattr(cls, name)
|
|
||||||
return cls.__module__ + '.' + cls.__name__ + '.' + m.__name__
|
|
||||||
|
|
||||||
|
|
||||||
# ============================== Helpers ==============================
|
|
||||||
|
|
||||||
|
|
||||||
bot.start(bot_token=os.environ['TG_TOKEN'])
|
|
||||||
bot.run_until_disconnected()
|
|
|
@ -1,382 +0,0 @@
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import asyncio
|
|
||||||
from getpass import getpass
|
|
||||||
|
|
||||||
from telethon.utils import get_display_name
|
|
||||||
|
|
||||||
from telethon import TelegramClient, events
|
|
||||||
from telethon.network import ConnectionTcpAbridged
|
|
||||||
from telethon.errors import SessionPasswordNeededError
|
|
||||||
|
|
||||||
|
|
||||||
# Create a global variable to hold the loop we will be using
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
|
|
||||||
def sprint(string, *args, **kwargs):
|
|
||||||
"""Safe Print (handle UnicodeEncodeErrors on some terminals)"""
|
|
||||||
try:
|
|
||||||
print(string, *args, **kwargs)
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
string = string.encode('utf-8', errors='ignore')\
|
|
||||||
.decode('ascii', errors='ignore')
|
|
||||||
print(string, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def print_title(title):
|
|
||||||
"""Helper function to print titles to the console more nicely"""
|
|
||||||
sprint('\n')
|
|
||||||
sprint('=={}=='.format('=' * len(title)))
|
|
||||||
sprint('= {} ='.format(title))
|
|
||||||
sprint('=={}=='.format('=' * len(title)))
|
|
||||||
|
|
||||||
|
|
||||||
def bytes_to_string(byte_count):
|
|
||||||
"""Converts a byte count to a string (in KB, MB...)"""
|
|
||||||
suffix_index = 0
|
|
||||||
while byte_count >= 1024:
|
|
||||||
byte_count /= 1024
|
|
||||||
suffix_index += 1
|
|
||||||
|
|
||||||
return '{:.2f}{}'.format(
|
|
||||||
byte_count, [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_input(prompt):
|
|
||||||
"""
|
|
||||||
Python's ``input()`` is blocking, which means the event loop we set
|
|
||||||
above can't be running while we're blocking there. This method will
|
|
||||||
let the loop run while we wait for input.
|
|
||||||
"""
|
|
||||||
print(prompt, end='', flush=True)
|
|
||||||
return (await loop.run_in_executor(None, sys.stdin.readline)).rstrip()
|
|
||||||
|
|
||||||
|
|
||||||
class InteractiveTelegramClient(TelegramClient):
|
|
||||||
"""Full featured Telegram client, meant to be used on an interactive
|
|
||||||
session to see what Telethon is capable off -
|
|
||||||
|
|
||||||
This client allows the user to perform some basic interaction with
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
Initializes the InteractiveTelegramClient.
|
|
||||||
:param session_user_id: Name of the *.session file.
|
|
||||||
:param user_phone: The phone of the user that will login.
|
|
||||||
:param api_id: Telegram's api_id acquired through my.telegram.org.
|
|
||||||
:param api_hash: Telegram's api_hash.
|
|
||||||
:param proxy: Optional proxy tuple/dictionary.
|
|
||||||
"""
|
|
||||||
print_title('Initialization')
|
|
||||||
|
|
||||||
print('Initializing interactive example...')
|
|
||||||
|
|
||||||
# The first step is to initialize the TelegramClient, as we are
|
|
||||||
# subclassing it, we need to call super().__init__(). On a more
|
|
||||||
# normal case you would want 'client = TelegramClient(...)'
|
|
||||||
super().__init__(
|
|
||||||
# These parameters should be passed always, session name and API
|
|
||||||
session_user_id, api_id, api_hash,
|
|
||||||
|
|
||||||
# You can optionally change the connection mode by passing a
|
|
||||||
# type or an instance of it. This changes how the sent packets
|
|
||||||
# look (low-level concept you normally shouldn't worry about).
|
|
||||||
# Default is ConnectionTcpFull, smallest is ConnectionTcpAbridged.
|
|
||||||
connection=ConnectionTcpAbridged,
|
|
||||||
|
|
||||||
# If you're using a proxy, set it here.
|
|
||||||
proxy=proxy
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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 raise a connection error False, so you need
|
|
||||||
# to except those before continuing. Otherwise you may want to retry
|
|
||||||
# as done here.
|
|
||||||
print('Connecting to Telegram servers...')
|
|
||||||
try:
|
|
||||||
loop.run_until_complete(self.connect())
|
|
||||||
except ConnectionError:
|
|
||||||
print('Initial connection failed. Retrying...')
|
|
||||||
loop.run_until_complete(self.connect())
|
|
||||||
|
|
||||||
# If the user hasn't called .sign_in() or .sign_up() yet, they won't
|
|
||||||
# be authorized. The first thing you must do is authorize. Calling
|
|
||||||
# .sign_in() should only be done once as the information is saved on
|
|
||||||
# the *.session file so you don't need to enter the code every time.
|
|
||||||
if not loop.run_until_complete(self.is_user_authorized()):
|
|
||||||
print('First run. Sending code request...')
|
|
||||||
loop.run_until_complete(self.sign_in(user_phone))
|
|
||||||
|
|
||||||
self_user = None
|
|
||||||
while self_user is None:
|
|
||||||
code = input('Enter the code you just received: ')
|
|
||||||
try:
|
|
||||||
self_user =\
|
|
||||||
loop.run_until_complete(self.sign_in(code=code))
|
|
||||||
|
|
||||||
# Two-step verification may be enabled, and .sign_in will
|
|
||||||
# raise this error. If that's the case ask for the password.
|
|
||||||
# Note that getpass() may not work on PyCharm due to a bug,
|
|
||||||
# if that's the case simply change it for input().
|
|
||||||
except SessionPasswordNeededError:
|
|
||||||
pw = getpass('Two step verification is enabled. '
|
|
||||||
'Please enter your password: ')
|
|
||||||
|
|
||||||
self_user =\
|
|
||||||
loop.run_until_complete(self.sign_in(password=pw))
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""Main loop of the TelegramClient, will wait for user action"""
|
|
||||||
|
|
||||||
# Once everything is ready, we can add an event handler.
|
|
||||||
#
|
|
||||||
# Events are an abstraction over Telegram's "Updates" and
|
|
||||||
# are much easier to use.
|
|
||||||
self.add_event_handler(self.message_handler, events.NewMessage)
|
|
||||||
|
|
||||||
# Enter a while loop to chat as long as the user wants
|
|
||||||
while True:
|
|
||||||
# Retrieve the top dialogs. You can set the limit to None to
|
|
||||||
# retrieve all of them if you wish, but beware that may take
|
|
||||||
# a long time if you have hundreds of them.
|
|
||||||
dialog_count = 15
|
|
||||||
|
|
||||||
# Entities represent the user, chat or channel
|
|
||||||
# corresponding to the dialog on the same index.
|
|
||||||
dialogs = await 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, 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()
|
|
||||||
print('> Who do you want to send messages to?')
|
|
||||||
print('> Available commands:')
|
|
||||||
print(' !q: Quits the dialogs window and exits.')
|
|
||||||
print(' !l: Logs out, terminating this session.')
|
|
||||||
print()
|
|
||||||
i = await async_input('Enter dialog ID or a command: ')
|
|
||||||
if i == '!q':
|
|
||||||
return
|
|
||||||
if i == '!l':
|
|
||||||
# Logging out will cause the user to need to reenter the
|
|
||||||
# code next time they want to use the library, and will
|
|
||||||
# also delete the *.session file off the filesystem.
|
|
||||||
#
|
|
||||||
# This is not the same as simply calling .disconnect(),
|
|
||||||
# which simply shuts down everything gracefully.
|
|
||||||
await self.log_out()
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
i = int(i if i else 0) - 1
|
|
||||||
# Ensure it is inside the bounds, otherwise retry
|
|
||||||
if not 0 <= i < dialog_count:
|
|
||||||
i = None
|
|
||||||
except ValueError:
|
|
||||||
i = None
|
|
||||||
|
|
||||||
# Retrieve the selected user (or chat, or channel)
|
|
||||||
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(' !dp: Downloads the current dialog Profile picture.')
|
|
||||||
print(' !i: Prints information about this chat..')
|
|
||||||
print()
|
|
||||||
|
|
||||||
# And start a while loop to chat
|
|
||||||
while True:
|
|
||||||
msg = await async_input('Enter a message: ')
|
|
||||||
# Quit
|
|
||||||
if msg == '!q':
|
|
||||||
break
|
|
||||||
elif msg == '!Q':
|
|
||||||
return
|
|
||||||
|
|
||||||
# History
|
|
||||||
elif msg == '!h':
|
|
||||||
# First retrieve the messages and some information
|
|
||||||
messages = await self.get_messages(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 in reversed(messages):
|
|
||||||
# Note how we access .sender here. Since we made an
|
|
||||||
# API call using the self client, it will always have
|
|
||||||
# information about the sender. This is different to
|
|
||||||
# events, where Telegram may not always send the user.
|
|
||||||
name = get_display_name(msg.sender)
|
|
||||||
|
|
||||||
# Format the message content
|
|
||||||
if getattr(msg, 'media', None):
|
|
||||||
self.found_media[msg.id] = msg
|
|
||||||
content = '<{}> {}'.format(
|
|
||||||
type(msg.media).__name__, msg.message)
|
|
||||||
|
|
||||||
elif hasattr(msg, 'message'):
|
|
||||||
content = msg.message
|
|
||||||
elif hasattr(msg, 'action'):
|
|
||||||
content = str(msg.action)
|
|
||||||
else:
|
|
||||||
# Unknown message, simply print its class name
|
|
||||||
content = type(msg).__name__
|
|
||||||
|
|
||||||
# And print it to the user
|
|
||||||
sprint('[{}:{}] (ID={}) {}: {}'.format(
|
|
||||||
msg.date.hour, msg.date.minute, msg.id, name, content))
|
|
||||||
|
|
||||||
# Send photo
|
|
||||||
elif msg.startswith('!up '):
|
|
||||||
# Slice the message to get the path
|
|
||||||
path = msg[len('!up '):]
|
|
||||||
await self.send_photo(path=path, entity=entity)
|
|
||||||
|
|
||||||
# Send file (document)
|
|
||||||
elif msg.startswith('!uf '):
|
|
||||||
# Slice the message to get the path
|
|
||||||
path = msg[len('!uf '):]
|
|
||||||
await self.send_document(path=path, entity=entity)
|
|
||||||
|
|
||||||
# Delete messages
|
|
||||||
elif msg.startswith('!d '):
|
|
||||||
# Slice the message to get message ID
|
|
||||||
msg = msg[len('!d '):]
|
|
||||||
deleted_msg = await self.delete_messages(entity, msg)
|
|
||||||
print('Deleted {}'.format(deleted_msg))
|
|
||||||
|
|
||||||
# Download media
|
|
||||||
elif msg.startswith('!dm '):
|
|
||||||
# Slice the message to get message ID
|
|
||||||
await self.download_media_by_id(msg[len('!dm '):])
|
|
||||||
|
|
||||||
# Download profile photo
|
|
||||||
elif msg == '!dp':
|
|
||||||
print('Downloading profile picture to usermedia/...')
|
|
||||||
os.makedirs('usermedia', exist_ok=True)
|
|
||||||
output = await self.download_profile_photo(entity,
|
|
||||||
'usermedia')
|
|
||||||
if output:
|
|
||||||
print('Profile picture downloaded to', output)
|
|
||||||
else:
|
|
||||||
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:
|
|
||||||
await self.send_message(entity, msg, link_preview=False)
|
|
||||||
|
|
||||||
async def send_photo(self, path, entity):
|
|
||||||
"""Sends the file located at path to the desired entity as a photo"""
|
|
||||||
await self.send_file(
|
|
||||||
entity, path,
|
|
||||||
progress_callback=self.upload_progress_callback
|
|
||||||
)
|
|
||||||
print('Photo sent!')
|
|
||||||
|
|
||||||
async def send_document(self, path, entity):
|
|
||||||
"""Sends the file located at path to the desired entity as a document"""
|
|
||||||
await self.send_file(
|
|
||||||
entity, path,
|
|
||||||
force_document=True,
|
|
||||||
progress_callback=self.upload_progress_callback
|
|
||||||
)
|
|
||||||
print('Document sent!')
|
|
||||||
|
|
||||||
async def download_media_by_id(self, media_id):
|
|
||||||
"""Given a message ID, finds the media this message contained and
|
|
||||||
downloads it.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
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
|
|
||||||
|
|
||||||
print('Downloading media to usermedia/...')
|
|
||||||
os.makedirs('usermedia', exist_ok=True)
|
|
||||||
output = await 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):
|
|
||||||
InteractiveTelegramClient.print_progress(
|
|
||||||
'Downloaded', downloaded_bytes, total_bytes
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def upload_progress_callback(uploaded_bytes, total_bytes):
|
|
||||||
InteractiveTelegramClient.print_progress(
|
|
||||||
'Uploaded', uploaded_bytes, total_bytes
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def print_progress(progress_type, downloaded_bytes, total_bytes):
|
|
||||||
print('{} {} out of {} ({:.2%})'.format(
|
|
||||||
progress_type, bytes_to_string(downloaded_bytes),
|
|
||||||
bytes_to_string(total_bytes), downloaded_bytes / total_bytes)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def message_handler(self, event):
|
|
||||||
"""Callback method for received events.NewMessage"""
|
|
||||||
|
|
||||||
# Note that message_handler is called when a Telegram update occurs
|
|
||||||
# and an event is created. Telegram may not always send information
|
|
||||||
# about the ``.sender`` or the ``.chat``, so if you *really* want it
|
|
||||||
# you should use ``get_chat()`` and ``get_sender()`` while working
|
|
||||||
# with events. Since they are methods, you know they may make an API
|
|
||||||
# call, which can be expensive.
|
|
||||||
chat = await event.get_chat()
|
|
||||||
if event.is_group:
|
|
||||||
if event.out:
|
|
||||||
sprint('>> sent "{}" to chat {}'.format(
|
|
||||||
event.text, get_display_name(chat)
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
sprint('<< {} @ {} sent "{}"'.format(
|
|
||||||
get_display_name(await event.get_sender()),
|
|
||||||
get_display_name(chat),
|
|
||||||
event.text
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
if event.out:
|
|
||||||
sprint('>> "{}" to user {}'.format(
|
|
||||||
event.text, get_display_name(chat)
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
sprint('<< {} sent "{}"'.format(
|
|
||||||
get_display_name(chat), event.text
|
|
||||||
))
|
|
|
@ -1,36 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# A simple script to print all updates received
|
|
||||||
#
|
|
||||||
# NOTE: To run this script you MUST have 'TG_API_ID' and 'TG_API_HASH' in
|
|
||||||
# your environment variables. This is a good way to use these private
|
|
||||||
# values. See https://superuser.com/q/284342.
|
|
||||||
from os import environ
|
|
||||||
|
|
||||||
from telethon import TelegramClient
|
|
||||||
|
|
||||||
|
|
||||||
client = TelegramClient(
|
|
||||||
environ.get('TG_SESSION', 'session'),
|
|
||||||
environ['TG_API_ID'],
|
|
||||||
environ['TG_API_HASH'],
|
|
||||||
proxy=None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_handler(update):
|
|
||||||
print(update)
|
|
||||||
|
|
||||||
|
|
||||||
client.add_event_handler(update_handler)
|
|
||||||
|
|
||||||
'''You could also have used the @client.on(...) syntax:
|
|
||||||
from telethon import events
|
|
||||||
|
|
||||||
@client.on(events.Raw)
|
|
||||||
async def update_handler(update):
|
|
||||||
print(update)
|
|
||||||
'''
|
|
||||||
|
|
||||||
with client.start():
|
|
||||||
print('(Press Ctrl+C to stop this)')
|
|
||||||
client.run_until_disconnected()
|
|
|
@ -1,81 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
A example script to automatically send messages based on certain triggers.
|
|
||||||
|
|
||||||
NOTE: To run this script you MUST have 'TG_API_ID' and 'TG_API_HASH' in
|
|
||||||
your environment variables. This is a good way to use these private
|
|
||||||
values. See https://superuser.com/q/284342.
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
from collections import defaultdict
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from os import environ
|
|
||||||
|
|
||||||
from telethon import TelegramClient, events
|
|
||||||
|
|
||||||
"""Uncomment this for debugging
|
|
||||||
import logging
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
logging.debug('dbg')
|
|
||||||
logging.info('info')
|
|
||||||
"""
|
|
||||||
|
|
||||||
REACTS = {'emacs': 'Needs more vim',
|
|
||||||
'chrome': 'Needs more Firefox'}
|
|
||||||
|
|
||||||
# A list of dates of reactions we've sent, so we can keep track of floods
|
|
||||||
recent_reacts = defaultdict(list)
|
|
||||||
|
|
||||||
|
|
||||||
# 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'],
|
|
||||||
proxy=None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async def my_handler(event: events.NewMessage.Event):
|
|
||||||
global recent_reacts
|
|
||||||
|
|
||||||
# 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[event.chat_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[event.chat_id] = [
|
|
||||||
a for a in recent_reacts[event.chat_id] if
|
|
||||||
datetime.now() - a < timedelta(minutes=10)
|
|
||||||
]
|
|
||||||
# Send a reaction as a reply (otherwise, event.respond())
|
|
||||||
await event.reply(response)
|
|
||||||
# Add this reaction to the list of recent actions
|
|
||||||
recent_reacts[event.chat_id].append(datetime.now())
|
|
||||||
|
|
||||||
# Automatically send relevant media when we say certain things
|
|
||||||
# When invoking requests, get_input_entity needs to be called manually
|
|
||||||
if event.out:
|
|
||||||
chat = await event.get_input_chat()
|
|
||||||
if event.raw_text.lower() == 'x files theme':
|
|
||||||
await client.send_file(chat, 'xfiles.m4a',
|
|
||||||
reply_to=event.message.id, voice_note=True)
|
|
||||||
if event.raw_text.lower() == 'anytime':
|
|
||||||
await client.send_file(chat, 'anytime.png',
|
|
||||||
reply_to=event.message.id)
|
|
||||||
if '.shrug' in event.text:
|
|
||||||
await event.edit(event.text.replace('.shrug', r'¯\_(ツ)_/¯'))
|
|
||||||
|
|
||||||
|
|
||||||
with client.start():
|
|
||||||
print('(Press Ctrl+C to stop this)')
|
|
||||||
client.run_until_disconnected()
|
|
|
@ -15,13 +15,13 @@ AUTO_GEN_NOTICE = \
|
||||||
|
|
||||||
AUTO_CASTS = {
|
AUTO_CASTS = {
|
||||||
'InputPeer':
|
'InputPeer':
|
||||||
'utils.get_input_peer(await client.get_input_entity({}))',
|
'utils.get_input_peer(client.get_input_entity({}))',
|
||||||
'InputChannel':
|
'InputChannel':
|
||||||
'utils.get_input_channel(await client.get_input_entity({}))',
|
'utils.get_input_channel(client.get_input_entity({}))',
|
||||||
'InputUser':
|
'InputUser':
|
||||||
'utils.get_input_user(await client.get_input_entity({}))',
|
'utils.get_input_user(client.get_input_entity({}))',
|
||||||
'InputDialogPeer':
|
'InputDialogPeer':
|
||||||
'utils.get_input_dialog(await client.get_input_entity({}))',
|
'utils.get_input_dialog(client.get_input_entity({}))',
|
||||||
|
|
||||||
'InputMedia': 'utils.get_input_media({})',
|
'InputMedia': 'utils.get_input_media({})',
|
||||||
'InputPhoto': 'utils.get_input_photo({})',
|
'InputPhoto': 'utils.get_input_photo({})',
|
||||||
|
@ -233,7 +233,7 @@ def _write_class_init(tlobject, kind, type_constructors, builder):
|
||||||
|
|
||||||
def _write_resolve(tlobject, builder):
|
def _write_resolve(tlobject, builder):
|
||||||
if any(arg.type in AUTO_CASTS for arg in tlobject.real_args):
|
if any(arg.type in AUTO_CASTS for arg in tlobject.real_args):
|
||||||
builder.writeln('async def resolve(self, client, utils):')
|
builder.writeln('def resolve(self, client, utils):')
|
||||||
for arg in tlobject.real_args:
|
for arg in tlobject.real_args:
|
||||||
ac = AUTO_CASTS.get(arg.type, None)
|
ac = AUTO_CASTS.get(arg.type, None)
|
||||||
if not ac:
|
if not ac:
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
from .crypto_test import CryptoTests
|
|
||||||
from .network_test import NetworkTests
|
|
||||||
from .parser_test import ParserTests
|
|
||||||
from .tl_test import TLTests
|
|
||||||
from .utils_test import UtilsTests
|
|
|
@ -1,143 +0,0 @@
|
||||||
import unittest
|
|
||||||
from hashlib import sha1
|
|
||||||
|
|
||||||
import telethon.helpers as utils
|
|
||||||
from telethon.crypto import AES, Factorization
|
|
||||||
# from crypto.PublicKey import RSA as PyCryptoRSA
|
|
||||||
|
|
||||||
|
|
||||||
class CryptoTests(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
# Test known values
|
|
||||||
self.key = b'\xd1\xf4MXy\x0c\xf8/z,\xe9\xf9\xa4\x17\x04\xd9C\xc9\xaba\x81\xf3\xf8\xdd\xcb\x0c6\x92\x01\x1f\xc2y'
|
|
||||||
self.iv = b':\x02\x91x\x90Dj\xa6\x03\x90C\x08\x9e@X\xb5E\xffwy\xf3\x1c\xde\xde\xfbo\x8dm\xd6e.Z'
|
|
||||||
|
|
||||||
self.plain_text = b'Non encrypted text :D'
|
|
||||||
self.plain_text_padded = b'My len is more uniform, promise!'
|
|
||||||
|
|
||||||
self.cipher_text = b'\xb6\xa7\xec.\xb9\x9bG\xcb\xe9{\x91[\x12\xfc\x84D\x1c' \
|
|
||||||
b'\x93\xd9\x17\x03\xcd\xd6\xb1D?\x98\xd2\xb5\xa5U\xfd'
|
|
||||||
|
|
||||||
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'"
|
|
||||||
|
|
||||||
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'
|
|
||||||
|
|
||||||
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
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
self.assertEqual(value, self.plain_text_padded,
|
|
||||||
msg='Decrypted text ("{}") does not equal expected ("{}")'
|
|
||||||
.format(value, self.plain_text_padded))
|
|
||||||
|
|
||||||
@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' \
|
|
||||||
b'\xdfJ\x14\x96\x98\x16\xa3G\xab\x04\x14!\\\xeb\n\xbcn\xdf\xc4%' \
|
|
||||||
b'\xc6\t\xb7\x16\x14\x9c\'\x81\x15=\xb0\xaf\x0e\x0bR\xaa\x0466s' \
|
|
||||||
b'\xf0\xcf\xb7\xb8>,D\x94x\xd7\xf8\xe0\x84\xcb%\xd3\x05\xb2\xe8' \
|
|
||||||
b'\x95Mr?\xa2\xe8In\xf9\x0b[E\x9b\xaa\x0cX\x7f\x0ei\xde\xeed\x1d' \
|
|
||||||
b'x/J\xce\xea^}0;\xa83B\xbbR\xa1\xbfe\x04\xb9\x1e\xa1"f=\xa5M@' \
|
|
||||||
b'\x9e\xdd\x81\x80\xc9\xa5\xfb\xfcg\xdd\x15\x03p!\x0ffD\x16\x892' \
|
|
||||||
b'\xea\xca\xb1A\x99O\xa94P\xa9\xa2\xc6;\xb2C9\x1dC5\xd2\r\xecL' \
|
|
||||||
b'\xd9\xabw-\x03\ry\xc2v\x17]\x02\x15\x0cBa\x97\xce\xa5\xb1\xe4]' \
|
|
||||||
b'\x8e\xe0,\xcfC{o\xfa\x99f\xa4pM\x00'
|
|
||||||
|
|
||||||
# Calculate key being the client
|
|
||||||
msg_key = b'\xba\x1a\xcf\xda\xa8^Cbl\xfa\xb6\x0c:\x9b\xb0\xfc'
|
|
||||||
|
|
||||||
key, iv = utils.calc_key(shared_key, msg_key, client=True)
|
|
||||||
expected_key = b"\xaf\xe3\x84Qm\xe0!\x0c\xd91\xe4\x9a\xa0v_gc" \
|
|
||||||
b"x\xa1\xb0\xc9\xbc\x16'v\xcf,\x9dM\xae\xc6\xa5"
|
|
||||||
|
|
||||||
expected_iv = b'\xb8Q\xf3\xc5\xa3]\xc6\xdf\x9e\xe0Q\xbd"\x8d' \
|
|
||||||
b'\x13\t\x0e\x9a\x9d^8\xa2\xf8\xe7\x00w\xd9\xc1' \
|
|
||||||
b'\xa7\xa0\xf7\x0f'
|
|
||||||
|
|
||||||
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]'
|
|
||||||
|
|
||||||
key, iv = utils.calc_key(shared_key, msg_key, client=False)
|
|
||||||
expected_key = b'\xdd0X\xb6\x93\x8e\xc9y\xef\x83\xf8\x8cj' \
|
|
||||||
b'\xa7h\x03\xe2\xc6\xb16\xc5\xbb\xfc\xe7' \
|
|
||||||
b'\xdf\xd6\xb1g\xf7u\xcfk'
|
|
||||||
|
|
||||||
expected_iv = b'\xdcL\xc2\x18\x01J"X\x86lb\xb6\xb547\xfd' \
|
|
||||||
b'\xe2a4\xb6\xaf}FS\xd7[\xe0N\r\x19\xfb\xbc'
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
key, iv = utils.generate_key_data_from_nonce(server_nonce, new_nonce)
|
|
||||||
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 '
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
# 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'
|
|
||||||
|
|
||||||
def test_factorize(self):
|
|
||||||
pq = 3118979781119966969
|
|
||||||
p, q = Factorization.factorize(pq)
|
|
||||||
if p > q:
|
|
||||||
p, q = q, p
|
|
||||||
|
|
||||||
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')
|
|
|
@ -1,49 +0,0 @@
|
||||||
import unittest
|
|
||||||
import os
|
|
||||||
from io import BytesIO
|
|
||||||
from random import randint
|
|
||||||
from hashlib import sha256
|
|
||||||
from telethon import TelegramClient
|
|
||||||
|
|
||||||
# Fill in your api_id and api_hash when running the tests
|
|
||||||
# and REMOVE THEM once you've finished testing them.
|
|
||||||
api_id = None
|
|
||||||
api_hash = None
|
|
||||||
|
|
||||||
|
|
||||||
class HigherLevelTests(unittest.TestCase):
|
|
||||||
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.set_dc(0, '149.154.167.40', 80)
|
|
||||||
self.assertTrue(client.connect())
|
|
||||||
|
|
||||||
try:
|
|
||||||
phone = '+999662' + str(randint(0, 9999)).zfill(4)
|
|
||||||
client.send_code_request(phone)
|
|
||||||
client.sign_up('22222', 'Test', 'DC')
|
|
||||||
|
|
||||||
me = client.get_me()
|
|
||||||
data = os.urandom(2 ** 17)
|
|
||||||
client.send_file(
|
|
||||||
me, data,
|
|
||||||
progress_callback=lambda c, t:
|
|
||||||
print('test_cdn_download:uploading {:.2%}...'.format(c/t))
|
|
||||||
)
|
|
||||||
msg = client.get_messages(me)[1][0]
|
|
||||||
|
|
||||||
out = BytesIO()
|
|
||||||
client.download_media(msg, out)
|
|
||||||
self.assertEqual(sha256(data).digest(), sha256(out.getvalue()).digest())
|
|
||||||
|
|
||||||
out = BytesIO()
|
|
||||||
client.download_media(msg, out) # Won't redirect
|
|
||||||
self.assertEqual(sha256(data).digest(), sha256(out.getvalue()).digest())
|
|
||||||
|
|
||||||
client.log_out()
|
|
||||||
finally:
|
|
||||||
client.disconnect()
|
|
|
@ -1,44 +0,0 @@
|
||||||
import random
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import telethon.network.authenticator as authenticator
|
|
||||||
from telethon.extensions import TcpClient
|
|
||||||
from telethon.network import Connection
|
|
||||||
|
|
||||||
|
|
||||||
def run_server_echo_thread(port):
|
|
||||||
def server_thread():
|
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
||||||
s.bind(('', port))
|
|
||||||
s.listen(1)
|
|
||||||
connection, address = s.accept()
|
|
||||||
with connection:
|
|
||||||
data = connection.recv(16)
|
|
||||||
connection.send(data)
|
|
||||||
|
|
||||||
server = threading.Thread(target=server_thread)
|
|
||||||
server.start()
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkTests(unittest.TestCase):
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
msg = b'Unit testing...'
|
|
||||||
client = TcpClient()
|
|
||||||
client.connect('localhost', port)
|
|
||||||
client.write(msg)
|
|
||||||
self.assertEqual(msg, client.read(15),
|
|
||||||
msg='Read message does not equal sent message')
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
@unittest.skip("Some parameters changed, so IP doesn't go there anymore.")
|
|
||||||
def test_authenticator(self):
|
|
||||||
transport = Connection('149.154.167.91', 443)
|
|
||||||
self.assertTrue(authenticator.do_authentication(transport))
|
|
||||||
transport.close()
|
|
|
@ -1,8 +0,0 @@
|
||||||
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)
|
|
|
@ -1,8 +0,0 @@
|
||||||
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)
|
|
|
@ -1,66 +0,0 @@
|
||||||
import os
|
|
||||||
import unittest
|
|
||||||
from telethon.tl import TLObject
|
|
||||||
from telethon.extensions import BinaryReader
|
|
||||||
|
|
||||||
|
|
||||||
class UtilsTests(unittest.TestCase):
|
|
||||||
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 ' \
|
|
||||||
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \
|
|
||||||
b'\x00\x80'
|
|
||||||
|
|
||||||
with BinaryReader(data) as reader:
|
|
||||||
value = reader.read_byte()
|
|
||||||
self.assertEqual(value, 1,
|
|
||||||
msg='Example byte should be 1 but is {}'.format(value))
|
|
||||||
|
|
||||||
value = reader.read_int()
|
|
||||||
self.assertEqual(value, 5,
|
|
||||||
msg='Example integer should be 5 but is {}'.format(value))
|
|
||||||
|
|
||||||
value = reader.read_long()
|
|
||||||
self.assertEqual(value, 13,
|
|
||||||
msg='Example long integer should be 13 but is {}'.format(value))
|
|
||||||
|
|
||||||
value = reader.read_float()
|
|
||||||
self.assertEqual(value, 17.0,
|
|
||||||
msg='Example float should be 17.0 but is {}'.format(value))
|
|
||||||
|
|
||||||
value = reader.read_double()
|
|
||||||
self.assertEqual(value, 25.0,
|
|
||||||
msg='Example double should be 25.0 but is {}'.format(value))
|
|
||||||
|
|
||||||
value = reader.read(7)
|
|
||||||
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)
|
|
||||||
self.assertEqual(value, 2**127,
|
|
||||||
msg='Example large integer should be {} but is {}'.format(2**127, value))
|
|
||||||
|
|
||||||
def test_binary_tgwriter_tgreader(self):
|
|
||||||
small_data = os.urandom(33)
|
|
||||||
small_data_padded = os.urandom(19) # +1 byte for length = 20 (%4 = 0)
|
|
||||||
|
|
||||||
large_data = os.urandom(999)
|
|
||||||
large_data_padded = os.urandom(1024)
|
|
||||||
|
|
||||||
data = (small_data, small_data_padded, large_data, large_data_padded)
|
|
||||||
string = 'Testing Telegram strings, this should work properly!'
|
|
||||||
serialized = b''.join(TLObject.serialize_bytes(d) for d in data) + \
|
|
||||||
TLObject.serialize_bytes(string)
|
|
||||||
|
|
||||||
with BinaryReader(serialized) as reader:
|
|
||||||
# And then try reading it without errors (it should be unharmed!)
|
|
||||||
for datum in data:
|
|
||||||
value = reader.tgread_bytes()
|
|
||||||
self.assertEqual(value, datum,
|
|
||||||
msg='Example bytes should be {} but is {}'.format(datum, value))
|
|
||||||
|
|
||||||
value = reader.tgread_string()
|
|
||||||
self.assertEqual(value, string,
|
|
||||||
msg='Example string should be {} but is {}'.format(string, value))
|
|
|
@ -1,53 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from telethon_examples.interactive_telegram_client \
|
|
||||||
import InteractiveTelegramClient
|
|
||||||
|
|
||||||
|
|
||||||
def load_settings(path='api/settings'):
|
|
||||||
"""Loads the user settings located under `api/`"""
|
|
||||||
result = {}
|
|
||||||
with open(path, 'r', encoding='utf-8') as file:
|
|
||||||
for line in file:
|
|
||||||
value_pair = line.split('=')
|
|
||||||
left = value_pair[0].strip()
|
|
||||||
right = value_pair[1].strip()
|
|
||||||
if right.isnumeric():
|
|
||||||
result[left] = int(right)
|
|
||||||
else:
|
|
||||||
result[left] = right
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Load the settings and initialize the client
|
|
||||||
settings = load_settings()
|
|
||||||
kwargs = {}
|
|
||||||
if settings.get('socks_proxy'):
|
|
||||||
import socks # $ pip install pysocks
|
|
||||||
host, port = settings['socks_proxy'].split(':')
|
|
||||||
kwargs = dict(proxy=(socks.SOCKS5, host, int(port)))
|
|
||||||
|
|
||||||
client = InteractiveTelegramClient(
|
|
||||||
session_user_id=str(settings.get('session_name', 'anonymous')),
|
|
||||||
user_phone=str(settings['user_phone']),
|
|
||||||
api_id=settings['api_id'],
|
|
||||||
api_hash=str(settings['api_hash']),
|
|
||||||
**kwargs)
|
|
||||||
|
|
||||||
print('Initialization done!')
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
try:
|
|
||||||
loop.run_until_complete(client.run())
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print('Unexpected error ({}): {} at\n{}'.format(
|
|
||||||
type(e), e, traceback.format_exc()))
|
|
||||||
|
|
||||||
finally:
|
|
||||||
loop.run_until_complete(client.disconnect())
|
|
||||||
print('Thanks for trying the interactive example! Exiting...')
|
|
Loading…
Reference in New Issue
Block a user