mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-02 11:10:18 +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!
|
||||
|
||||
|logo| **Telethon** is an `asyncio
|
||||
<https://docs.python.org/3/library/asyncio.html>`_ **Python 3** library
|
||||
to interact with Telegram's API.
|
||||
This is the threaded, simpler version of Telethon for people who
|
||||
can't bother learning ``asyncio`` but wouldn't like their scripts
|
||||
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**
|
||||
`this section of the documentation
|
||||
<https://telethon.readthedocs.io/en/latest/extra/basic/asyncio-magic.html>`_.
|
||||
|
||||
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
|
||||
Please consider learning ``asyncio``. The `documentation
|
||||
<http://telethon.rtfd.io/>`_ is the same for both versions
|
||||
of the library. Simply don't write any keywords like ``async``
|
||||
or ``await`` and you will be good.
|
||||
|
|
|
@ -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*'(.*)'.*$",
|
||||
f.read(), flags=re.MULTILINE).group(1)
|
||||
setup(
|
||||
name='Telethon',
|
||||
name='Telethon-sync',
|
||||
version=version,
|
||||
description="Full-featured Telegram client library for Python 3",
|
||||
long_description=long_description,
|
||||
|
@ -199,7 +199,7 @@ def main():
|
|||
# See https://stackoverflow.com/a/40300957/4759433
|
||||
# -> https://www.python.org/dev/peps/pep-0345/#requires-python
|
||||
# -> 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
|
||||
classifiers=[
|
||||
|
@ -214,6 +214,7 @@ def main():
|
|||
'License :: OSI Approved :: MIT License',
|
||||
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6'
|
||||
],
|
||||
|
|
|
@ -38,7 +38,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
(You are now logged in)
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
|
@ -109,17 +109,17 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
else self.loop.run_until_complete(coro)
|
||||
)
|
||||
|
||||
async def _start(
|
||||
def _start(
|
||||
self, phone, password, bot_token, force_sms,
|
||||
code_callback, first_name, last_name, max_attempts):
|
||||
if not self.is_connected():
|
||||
await self.connect()
|
||||
self.connect()
|
||||
|
||||
if await self.is_user_authorized():
|
||||
if self.is_user_authorized():
|
||||
return self
|
||||
|
||||
if bot_token:
|
||||
await self.sign_in(bot_token=bot_token)
|
||||
self.sign_in(bot_token=bot_token)
|
||||
return self
|
||||
|
||||
# Turn the callable into a valid phone number
|
||||
|
@ -130,16 +130,16 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
attempts = 0
|
||||
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
|
||||
while attempts < max_attempts:
|
||||
try:
|
||||
if sign_up:
|
||||
me = await self.sign_up(
|
||||
me = self.sign_up(
|
||||
code_callback(), first_name, last_name)
|
||||
else:
|
||||
# Raises SessionPasswordNeededError if 2FA enabled
|
||||
me = await self.sign_in(phone, code=code_callback())
|
||||
me = self.sign_in(phone, code=code_callback())
|
||||
break
|
||||
except errors.SessionPasswordNeededError:
|
||||
two_step_detected = True
|
||||
|
@ -171,7 +171,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
if callable(password):
|
||||
for _ in range(max_attempts):
|
||||
try:
|
||||
me = await self.sign_in(
|
||||
me = self.sign_in(
|
||||
phone=phone, password=password())
|
||||
break
|
||||
except errors.PasswordHashInvalidError:
|
||||
|
@ -180,7 +180,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
else:
|
||||
raise errors.PasswordHashInvalidError()
|
||||
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)
|
||||
signed, name = 'Signed in successfully as', utils.get_display_name(me)
|
||||
|
@ -193,7 +193,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
|
||||
return self
|
||||
|
||||
async def sign_in(
|
||||
def sign_in(
|
||||
self, phone=None, *, code=None, password=None,
|
||||
bot_token=None, phone_code_hash=None):
|
||||
"""
|
||||
|
@ -228,12 +228,12 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
The signed in user, or the information about
|
||||
:meth:`send_code_request`.
|
||||
"""
|
||||
me = await self.get_me()
|
||||
me = self.get_me()
|
||||
if me:
|
||||
return me
|
||||
|
||||
if phone and not code and not password:
|
||||
return await self.send_code_request(phone)
|
||||
return self.send_code_request(phone)
|
||||
elif code:
|
||||
phone = utils.parse_phone(phone) or self._phone
|
||||
phone_code_hash = \
|
||||
|
@ -248,16 +248,16 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
|
||||
# May raise PhoneCodeEmptyError, PhoneCodeExpiredError,
|
||||
# PhoneCodeHashEmptyError or PhoneCodeInvalidError.
|
||||
result = await self(functions.auth.SignInRequest(
|
||||
result = self(functions.auth.SignInRequest(
|
||||
phone, phone_code_hash, str(code)))
|
||||
elif password:
|
||||
salt = (await self(
|
||||
salt = (self(
|
||||
functions.account.GetPasswordRequest())).current_salt
|
||||
result = await self(functions.auth.CheckPasswordRequest(
|
||||
result = self(functions.auth.CheckPasswordRequest(
|
||||
helpers.get_password_hash(password, salt)
|
||||
))
|
||||
elif bot_token:
|
||||
result = await self(functions.auth.ImportBotAuthorizationRequest(
|
||||
result = self(functions.auth.ImportBotAuthorizationRequest(
|
||||
flags=0, bot_auth_token=bot_token,
|
||||
api_id=self.api_id, api_hash=self.api_hash
|
||||
))
|
||||
|
@ -273,7 +273,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
self._authorized = True
|
||||
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.
|
||||
You must call .send_code_request(phone) first.
|
||||
|
@ -296,7 +296,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
Returns:
|
||||
The new created :tl:`User`.
|
||||
"""
|
||||
me = await self.get_me()
|
||||
me = self.get_me()
|
||||
if me:
|
||||
return me
|
||||
|
||||
|
@ -308,7 +308,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
sys.stderr.write("{}\n".format(t))
|
||||
sys.stderr.flush()
|
||||
|
||||
result = await self(functions.auth.SignUpRequest(
|
||||
result = self(functions.auth.SignUpRequest(
|
||||
phone_number=self._phone,
|
||||
phone_code_hash=self._phone_code_hash.get(self._phone, ''),
|
||||
phone_code=str(code),
|
||||
|
@ -317,7 +317,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
))
|
||||
|
||||
if self._tos:
|
||||
await self(
|
||||
self(
|
||||
functions.help.AcceptTermsOfServiceRequest(self._tos.id))
|
||||
|
||||
self._self_input_peer = utils.get_input_peer(
|
||||
|
@ -326,7 +326,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
self._authorized = True
|
||||
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.
|
||||
|
||||
|
@ -345,7 +345,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
|
||||
if not phone_hash:
|
||||
try:
|
||||
result = await self(functions.auth.SendCodeRequest(
|
||||
result = self(functions.auth.SendCodeRequest(
|
||||
phone, self.api_id, self.api_hash))
|
||||
except errors.AuthRestartError:
|
||||
return self.send_code_request(phone, force_sms=force_sms)
|
||||
|
@ -358,14 +358,14 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
self._phone = phone
|
||||
|
||||
if force_sms:
|
||||
result = await self(
|
||||
result = self(
|
||||
functions.auth.ResendCodeRequest(phone, phone_hash))
|
||||
|
||||
self._phone_code_hash[phone] = result.phone_code_hash
|
||||
|
||||
return result
|
||||
|
||||
async def log_out(self):
|
||||
def log_out(self):
|
||||
"""
|
||||
Logs out Telegram and deletes the current ``*.session`` file.
|
||||
|
||||
|
@ -373,16 +373,16 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
``True`` if the operation was successful.
|
||||
"""
|
||||
try:
|
||||
await self(functions.auth.LogOutRequest())
|
||||
self(functions.auth.LogOutRequest())
|
||||
except errors.RPCError:
|
||||
return False
|
||||
|
||||
await self.disconnect()
|
||||
self.disconnect()
|
||||
self.session.delete()
|
||||
self._authorized = False
|
||||
return True
|
||||
|
||||
async def edit_2fa(
|
||||
def edit_2fa(
|
||||
self, current_password=None, new_password=None,
|
||||
*, hint='', email=None):
|
||||
"""
|
||||
|
@ -418,7 +418,7 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
if new_password is None and current_password is None:
|
||||
return False
|
||||
|
||||
pass_result = await self(functions.account.GetPasswordRequest())
|
||||
pass_result = self(functions.account.GetPasswordRequest())
|
||||
if isinstance(
|
||||
pass_result, types.account.NoPassword) and current_password:
|
||||
current_password = None
|
||||
|
@ -445,11 +445,11 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
)
|
||||
if email: # If enabling 2FA or changing email
|
||||
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
|
||||
))
|
||||
else: # Removing existing password
|
||||
return await self(functions.account.UpdatePasswordSettingsRequest(
|
||||
return self(functions.account.UpdatePasswordSettingsRequest(
|
||||
current_password_hash,
|
||||
new_settings=types.account.PasswordInputSettings(
|
||||
new_salt=bytes(),
|
||||
|
@ -465,13 +465,13 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
|||
def __enter__(self):
|
||||
return self.start()
|
||||
|
||||
async def __aenter__(self):
|
||||
return await self.start()
|
||||
def __aenter__(self):
|
||||
return self.start()
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.disconnect()
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
await self.disconnect()
|
||||
def __aexit__(self, *args):
|
||||
self.disconnect()
|
||||
|
||||
# endregion
|
||||
|
|
|
@ -12,7 +12,7 @@ class ChatMethods(UserMethods):
|
|||
# region Public methods
|
||||
|
||||
@async_generator
|
||||
async def iter_participants(
|
||||
def iter_participants(
|
||||
self, entity, limit=None, *, search='',
|
||||
filter=None, aggressive=False, _total=None):
|
||||
"""
|
||||
|
@ -61,7 +61,7 @@ class ChatMethods(UserMethods):
|
|||
else:
|
||||
filter = filter()
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
entity = self.get_input_entity(entity)
|
||||
if search and (filter
|
||||
or not isinstance(entity, types.InputPeerChannel)):
|
||||
# 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)
|
||||
if isinstance(entity, types.InputPeerChannel):
|
||||
if _total or (aggressive and not filter):
|
||||
total = (await self(functions.channels.GetFullChannelRequest(
|
||||
total = (self(functions.channels.GetFullChannelRequest(
|
||||
entity
|
||||
))).full_chat.participants_count
|
||||
if _total:
|
||||
|
@ -117,7 +117,7 @@ class ChatMethods(UserMethods):
|
|||
if requests[0].offset > limit:
|
||||
break
|
||||
|
||||
results = await self(requests)
|
||||
results = self(requests)
|
||||
for i in reversed(range(len(requests))):
|
||||
participants = results[i]
|
||||
if not participants.users:
|
||||
|
@ -133,12 +133,12 @@ class ChatMethods(UserMethods):
|
|||
seen.add(participant.user_id)
|
||||
user = users[participant.user_id]
|
||||
user.participant = participant
|
||||
await yield_(user)
|
||||
yield_(user)
|
||||
if len(seen) >= limit:
|
||||
return
|
||||
|
||||
elif isinstance(entity, types.InputPeerChat):
|
||||
full = await self(
|
||||
full = self(
|
||||
functions.messages.GetFullChatRequest(entity.chat_id))
|
||||
if not isinstance(
|
||||
full.full_chat.participants, types.ChatParticipants):
|
||||
|
@ -161,17 +161,17 @@ class ChatMethods(UserMethods):
|
|||
else:
|
||||
user = users[participant.user_id]
|
||||
user.participant = participant
|
||||
await yield_(user)
|
||||
yield_(user)
|
||||
else:
|
||||
if _total:
|
||||
_total[0] = 1
|
||||
if limit != 0:
|
||||
user = await self.get_entity(entity)
|
||||
user = self.get_entity(entity)
|
||||
if filter_entity(user):
|
||||
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
|
||||
with an additional ``.total`` attribute on the list.
|
||||
|
@ -179,7 +179,7 @@ class ChatMethods(UserMethods):
|
|||
total = [0]
|
||||
kwargs['_total'] = total
|
||||
participants = UserList()
|
||||
async for x in self.iter_participants(*args, **kwargs):
|
||||
for x in self.iter_participants(*args, **kwargs):
|
||||
participants.append(x)
|
||||
participants.total = total[0]
|
||||
return participants
|
||||
|
|
|
@ -13,7 +13,7 @@ class DialogMethods(UserMethods):
|
|||
# region Public methods
|
||||
|
||||
@async_generator
|
||||
async def iter_dialogs(
|
||||
def iter_dialogs(
|
||||
self, limit=None, *, offset_date=None, offset_id=0,
|
||||
offset_peer=types.InputPeerEmpty(), ignore_migrated=False,
|
||||
_total=None):
|
||||
|
@ -56,7 +56,7 @@ class DialogMethods(UserMethods):
|
|||
if not _total:
|
||||
return
|
||||
# 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_id=offset_id,
|
||||
offset_peer=offset_peer,
|
||||
|
@ -74,7 +74,7 @@ class DialogMethods(UserMethods):
|
|||
)
|
||||
while len(seen) < limit:
|
||||
req.limit = min(limit - len(seen), 100)
|
||||
r = await self(req)
|
||||
r = self(req)
|
||||
|
||||
if _total:
|
||||
_total[0] = getattr(r, 'count', len(r.dialogs))
|
||||
|
@ -97,7 +97,7 @@ class DialogMethods(UserMethods):
|
|||
|
||||
if not ignore_migrated or getattr(
|
||||
cd.entity, 'migrated_to', None) is None:
|
||||
await yield_(cd)
|
||||
yield_(cd)
|
||||
|
||||
if len(r.dialogs) < req.limit\
|
||||
or not isinstance(r, types.messages.DialogsSlice):
|
||||
|
@ -116,7 +116,7 @@ class DialogMethods(UserMethods):
|
|||
req.offset_id = r.messages[-1].id
|
||||
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
|
||||
with an additional ``.total`` attribute on the list.
|
||||
|
@ -124,13 +124,13 @@ class DialogMethods(UserMethods):
|
|||
total = [0]
|
||||
kwargs['_total'] = total
|
||||
dialogs = UserList()
|
||||
async for x in self.iter_dialogs(*args, **kwargs):
|
||||
for x in self.iter_dialogs(*args, **kwargs):
|
||||
dialogs.append(x)
|
||||
dialogs.total = total[0]
|
||||
return dialogs
|
||||
|
||||
@async_generator
|
||||
async def iter_drafts(self):
|
||||
def iter_drafts(self):
|
||||
"""
|
||||
Iterator over all open draft messages.
|
||||
|
||||
|
@ -139,16 +139,16 @@ class DialogMethods(UserMethods):
|
|||
to change the message or `telethon.tl.custom.draft.Draft.delete`
|
||||
among other things.
|
||||
"""
|
||||
r = await self(functions.messages.GetAllDraftsRequest())
|
||||
r = self(functions.messages.GetAllDraftsRequest())
|
||||
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.
|
||||
"""
|
||||
result = []
|
||||
async for x in self.iter_drafts():
|
||||
for x in self.iter_drafts():
|
||||
result.append(x)
|
||||
return result
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ class DownloadMethods(UserMethods):
|
|||
|
||||
# region Public methods
|
||||
|
||||
async def download_profile_photo(
|
||||
def download_profile_photo(
|
||||
self, entity, file=None, *, download_big=True):
|
||||
"""
|
||||
Downloads the profile photo of the given entity (user/chat/channel).
|
||||
|
@ -41,7 +41,7 @@ class DownloadMethods(UserMethods):
|
|||
# ('InputPeer', 'InputUser', 'InputChannel')
|
||||
INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd)
|
||||
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 = []
|
||||
if entity.SUBCLASS_OF_ID not in ENTITIES:
|
||||
|
@ -53,7 +53,7 @@ class DownloadMethods(UserMethods):
|
|||
if not hasattr(entity, 'chat_photo'):
|
||||
return None
|
||||
|
||||
return await self._download_photo(
|
||||
return self._download_photo(
|
||||
entity.chat_photo, file, date=None, progress_callback=None)
|
||||
|
||||
for attr in ('username', 'first_name', 'title'):
|
||||
|
@ -75,15 +75,15 @@ class DownloadMethods(UserMethods):
|
|||
)
|
||||
|
||||
try:
|
||||
await self.download_file(loc, file)
|
||||
self.download_file(loc, file)
|
||||
return file
|
||||
except errors.LocationInvalidError:
|
||||
# See issue #500, Android app fails as of v4.6.0 (1155).
|
||||
# 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):
|
||||
full = await self(functions.channels.GetFullChannelRequest(ie))
|
||||
return await self._download_photo(
|
||||
full = self(functions.channels.GetFullChannelRequest(ie))
|
||||
return self._download_photo(
|
||||
full.full_chat.chat_photo, file,
|
||||
date=None, progress_callback=None
|
||||
)
|
||||
|
@ -91,7 +91,7 @@ class DownloadMethods(UserMethods):
|
|||
# Until there's a report for chats, no need to.
|
||||
return None
|
||||
|
||||
async def download_media(self, message, file=None,
|
||||
def download_media(self, message, file=None,
|
||||
*, progress_callback=None):
|
||||
"""
|
||||
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,
|
||||
types.PhotoSize, types.PhotoCachedSize)):
|
||||
return await self._download_photo(
|
||||
return self._download_photo(
|
||||
media, file, date, progress_callback
|
||||
)
|
||||
elif isinstance(media, (types.MessageMediaDocument, types.Document)):
|
||||
return await self._download_document(
|
||||
return self._download_document(
|
||||
media, file, date, progress_callback
|
||||
)
|
||||
elif isinstance(media, types.MessageMediaContact):
|
||||
|
@ -141,7 +141,7 @@ class DownloadMethods(UserMethods):
|
|||
media, file
|
||||
)
|
||||
|
||||
async def download_file(
|
||||
def download_file(
|
||||
self, input_location, file=None, *, part_size_kb=None,
|
||||
file_size=None, progress_callback=None):
|
||||
"""
|
||||
|
@ -209,7 +209,7 @@ class DownloadMethods(UserMethods):
|
|||
offset = 0
|
||||
while True:
|
||||
try:
|
||||
result = await sender.send(functions.upload.GetFileRequest(
|
||||
result = sender.send(functions.upload.GetFileRequest(
|
||||
input_location, offset, part_size
|
||||
))
|
||||
if isinstance(result, types.upload.FileCdnRedirect):
|
||||
|
@ -217,7 +217,7 @@ class DownloadMethods(UserMethods):
|
|||
raise NotImplementedError
|
||||
except errors.FileMigrateError as e:
|
||||
__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
|
||||
|
||||
offset += part_size
|
||||
|
@ -234,7 +234,7 @@ class DownloadMethods(UserMethods):
|
|||
progress_callback(f.tell(), file_size)
|
||||
finally:
|
||||
if sender != self._sender:
|
||||
await sender.disconnect()
|
||||
sender.disconnect()
|
||||
if isinstance(file, str) or in_memory:
|
||||
f.close()
|
||||
|
||||
|
@ -242,7 +242,7 @@ class DownloadMethods(UserMethods):
|
|||
|
||||
# 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"""
|
||||
# Determine the photo and its largest size
|
||||
if isinstance(photo, types.MessageMediaPhoto):
|
||||
|
@ -272,12 +272,12 @@ class DownloadMethods(UserMethods):
|
|||
f.close()
|
||||
return file
|
||||
|
||||
await self.download_file(
|
||||
self.download_file(
|
||||
photo.location, file, file_size=photo.size,
|
||||
progress_callback=progress_callback)
|
||||
return file
|
||||
|
||||
async def _download_document(
|
||||
def _download_document(
|
||||
self, document, file, date, progress_callback):
|
||||
"""Specialized version of .download_media() for documents."""
|
||||
if isinstance(document, types.MessageMediaDocument):
|
||||
|
@ -311,7 +311,7 @@ class DownloadMethods(UserMethods):
|
|||
date=date, possible_names=possible_names
|
||||
)
|
||||
|
||||
await self.download_file(
|
||||
self.download_file(
|
||||
document, file, file_size=file_size,
|
||||
progress_callback=progress_callback)
|
||||
return file
|
||||
|
|
|
@ -45,7 +45,7 @@ class MessageParseMethods(UserMethods):
|
|||
|
||||
# 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``,
|
||||
or do nothing if it can't be found.
|
||||
|
@ -53,12 +53,12 @@ class MessageParseMethods(UserMethods):
|
|||
try:
|
||||
entities[i] = types.InputMessageEntityMentionName(
|
||||
entities[i].offset, entities[i].length,
|
||||
await self.get_input_entity(user)
|
||||
self.get_input_entity(user)
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
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``.
|
||||
"""
|
||||
|
@ -76,10 +76,10 @@ class MessageParseMethods(UserMethods):
|
|||
m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url)
|
||||
if m:
|
||||
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,
|
||||
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
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
# region Message retrieval
|
||||
|
||||
@async_generator
|
||||
async def iter_messages(
|
||||
def iter_messages(
|
||||
self, entity, limit=None, *, offset_date=None, offset_id=0,
|
||||
max_id=0, min_id=0, add_offset=0, search=None, filter=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
|
||||
# fetch the input version if we're not using IDs or if it was given.
|
||||
if not ids or entity:
|
||||
entity = await self.get_input_entity(entity)
|
||||
entity = self.get_input_entity(entity)
|
||||
|
||||
if ids:
|
||||
if not utils.is_list_like(ids):
|
||||
ids = (ids,)
|
||||
async for x in self._iter_ids(entity, ids, total=_total):
|
||||
await yield_(x)
|
||||
for x in self._iter_ids(entity, ids, total=_total):
|
||||
yield_(x)
|
||||
return
|
||||
|
||||
# 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,
|
||||
hash=0,
|
||||
from_id=(
|
||||
await self.get_input_entity(from_user)
|
||||
self.get_input_entity(from_user)
|
||||
if from_user else None
|
||||
)
|
||||
)
|
||||
|
@ -155,7 +155,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
# Telegram completely ignores `from_id` in private
|
||||
# chats, so we need to do this check client-side.
|
||||
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:
|
||||
from_id = request.from_id
|
||||
else:
|
||||
|
@ -174,7 +174,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
if not _total:
|
||||
return
|
||||
# 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):
|
||||
_total[0] = result.count
|
||||
else:
|
||||
|
@ -191,7 +191,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
start = asyncio.get_event_loop().time()
|
||||
# Telegram has a hard limit of 100
|
||||
request.limit = min(limit - have, batch_size)
|
||||
r = await self(request)
|
||||
r = self(request)
|
||||
if _total:
|
||||
_total[0] = getattr(r, 'count', len(r.messages))
|
||||
|
||||
|
@ -213,7 +213,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
# IDs are returned in descending order.
|
||||
last_id = message.id
|
||||
|
||||
await yield_(custom.Message(self, message, entities, entity))
|
||||
yield_(custom.Message(self, message, entities, entity))
|
||||
have += 1
|
||||
|
||||
if len(r.messages) < request.limit:
|
||||
|
@ -243,10 +243,10 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
request.max_date = last_message.date
|
||||
|
||||
now = asyncio.get_event_loop().time()
|
||||
await asyncio.sleep(
|
||||
asyncio.sleep(
|
||||
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
|
||||
with an additional ``.total`` attribute on the list.
|
||||
|
@ -272,7 +272,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
kwargs['limit'] = 1
|
||||
|
||||
msgs = UserList()
|
||||
async for x in self.iter_messages(*args, **kwargs):
|
||||
for x in self.iter_messages(*args, **kwargs):
|
||||
msgs.append(x)
|
||||
msgs.total = total[0]
|
||||
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
|
||||
|
||||
async def send_message(
|
||||
def send_message(
|
||||
self, entity, message='', *, reply_to=None,
|
||||
parse_mode=utils.Default, link_preview=True, file=None,
|
||||
force_document=False, clear_draft=False):
|
||||
|
@ -340,7 +340,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
The sent `telethon.tl.custom.message.Message`.
|
||||
"""
|
||||
if file is not None:
|
||||
return await self.send_file(
|
||||
return self.send_file(
|
||||
entity, file, caption=message, reply_to=reply_to,
|
||||
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'
|
||||
)
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
entity = self.get_input_entity(entity)
|
||||
if isinstance(message, types.Message):
|
||||
if (message.media and not isinstance(
|
||||
message.media, types.MessageMediaWebPage)):
|
||||
return await self.send_file(
|
||||
return self.send_file(
|
||||
entity, message.media, caption=message.message,
|
||||
entities=message.entities
|
||||
)
|
||||
|
@ -377,7 +377,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
)
|
||||
message = message.message
|
||||
else:
|
||||
message, msg_ent = await self._parse_message_text(message,
|
||||
message, msg_ent = self._parse_message_text(message,
|
||||
parse_mode)
|
||||
request = functions.messages.SendMessageRequest(
|
||||
peer=entity,
|
||||
|
@ -388,7 +388,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
clear_draft=clear_draft
|
||||
)
|
||||
|
||||
result = await self(request)
|
||||
result = self(request)
|
||||
if isinstance(result, types.UpdateShortSentMessage):
|
||||
to_id, cls = utils.resolve_id(utils.get_peer_id(entity))
|
||||
return custom.Message(self, types.Message(
|
||||
|
@ -403,7 +403,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
|
||||
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.
|
||||
|
||||
|
@ -448,7 +448,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
id=[m if isinstance(m, int) else m.id for m in messages],
|
||||
to_peer=entity
|
||||
)
|
||||
result = await self(req)
|
||||
result = self(req)
|
||||
if isinstance(result, (types.Updates, types.UpdatesCombined)):
|
||||
entities = {utils.get_peer_id(x): x
|
||||
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]
|
||||
return result[0] if single else result
|
||||
|
||||
async def edit_message(
|
||||
def edit_message(
|
||||
self, entity, message=None, text=None,
|
||||
*, parse_mode=utils.Default, link_preview=True, file=None):
|
||||
"""
|
||||
|
@ -527,9 +527,9 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
message = entity
|
||||
entity = entity.to_id
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
text, msg_entities = await self._parse_message_text(text, parse_mode)
|
||||
file_handle, media = await self._file_to_media(file)
|
||||
entity = self.get_input_entity(entity)
|
||||
text, msg_entities = self._parse_message_text(text, parse_mode)
|
||||
file_handle, media = self._file_to_media(file)
|
||||
request = functions.messages.EditMessageRequest(
|
||||
peer=entity,
|
||||
id=utils.get_message_id(message),
|
||||
|
@ -538,11 +538,11 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
entities=msg_entities,
|
||||
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)
|
||||
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".
|
||||
|
||||
|
@ -574,19 +574,19 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
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):
|
||||
return await self([functions.channels.DeleteMessagesRequest(
|
||||
return self([functions.channels.DeleteMessagesRequest(
|
||||
entity, list(c)) for c in utils.chunks(message_ids)])
|
||||
else:
|
||||
return await self([functions.messages.DeleteMessagesRequest(
|
||||
return self([functions.messages.DeleteMessagesRequest(
|
||||
list(c), revoke) for c in utils.chunks(message_ids)])
|
||||
|
||||
# endregion
|
||||
|
||||
# region Miscellaneous
|
||||
|
||||
async def send_read_acknowledge(
|
||||
def send_read_acknowledge(
|
||||
self, entity, message=None, *, max_id=None, clear_mentions=False):
|
||||
"""
|
||||
Sends a "read acknowledge" (i.e., notifying the given peer that we've
|
||||
|
@ -623,18 +623,18 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
raise ValueError(
|
||||
'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:
|
||||
await self(functions.messages.ReadMentionsRequest(entity))
|
||||
self(functions.messages.ReadMentionsRequest(entity))
|
||||
if max_id is None:
|
||||
return True
|
||||
|
||||
if max_id is not None:
|
||||
if isinstance(entity, types.InputPeerChannel):
|
||||
return await self(functions.channels.ReadHistoryRequest(
|
||||
return self(functions.channels.ReadHistoryRequest(
|
||||
entity, max_id=max_id))
|
||||
else:
|
||||
return await self(functions.messages.ReadHistoryRequest(
|
||||
return self(functions.messages.ReadHistoryRequest(
|
||||
entity, max_id=max_id))
|
||||
|
||||
return False
|
||||
|
@ -646,7 +646,7 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
# region Private methods
|
||||
|
||||
@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.
|
||||
"""
|
||||
|
@ -655,15 +655,15 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
|
||||
from_id = None # By default, no need to validate from_id
|
||||
if isinstance(entity, types.InputPeerChannel):
|
||||
r = await self(functions.channels.GetMessagesRequest(entity, ids))
|
||||
r = self(functions.channels.GetMessagesRequest(entity, ids))
|
||||
else:
|
||||
r = await self(functions.messages.GetMessagesRequest(ids))
|
||||
r = self(functions.messages.GetMessagesRequest(ids))
|
||||
if entity:
|
||||
from_id = utils.get_peer_id(entity)
|
||||
|
||||
if isinstance(r, types.messages.MessagesNotModified):
|
||||
for _ in ids:
|
||||
await yield_(None)
|
||||
yield_(None)
|
||||
return
|
||||
|
||||
entities = {utils.get_peer_id(x): x
|
||||
|
@ -678,8 +678,8 @@ class MessageMethods(UploadMethods, MessageParseMethods):
|
|||
for message in r.messages:
|
||||
if isinstance(message, types.MessageEmpty) or (
|
||||
from_id and utils.get_peer_id(message.to_id) != from_id):
|
||||
await yield_(None)
|
||||
yield_(None)
|
||||
else:
|
||||
await yield_(custom.Message(self, message, entities, entity))
|
||||
yield_(custom.Message(self, message, entities, entity))
|
||||
|
||||
# endregion
|
||||
|
|
|
@ -269,14 +269,14 @@ class TelegramBaseClient(abc.ABC):
|
|||
|
||||
# region Connecting
|
||||
|
||||
async def connect(self):
|
||||
def connect(self):
|
||||
"""
|
||||
Connects to Telegram.
|
||||
"""
|
||||
await self._sender.connect(
|
||||
self._sender.connect(
|
||||
self.session.server_address, self.session.port)
|
||||
|
||||
await self._sender.send(self._init_with(
|
||||
self._sender.send(self._init_with(
|
||||
functions.help.GetConfigRequest()))
|
||||
|
||||
self._updates_handle = self._loop.create_task(self._update_loop())
|
||||
|
@ -287,15 +287,15 @@ class TelegramBaseClient(abc.ABC):
|
|||
"""
|
||||
return self._sender.is_connected()
|
||||
|
||||
async def disconnect(self):
|
||||
def disconnect(self):
|
||||
"""
|
||||
Disconnects from Telegram.
|
||||
"""
|
||||
await self._disconnect()
|
||||
self._disconnect()
|
||||
if getattr(self, 'session', None):
|
||||
self.session.close()
|
||||
|
||||
async def _disconnect(self):
|
||||
def _disconnect(self):
|
||||
"""
|
||||
Disconnect only, without closing the session. Used in reconnections
|
||||
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
|
||||
# method will be called from `__del__` which would crash then.
|
||||
if getattr(self, '_sender', None):
|
||||
await self._sender.disconnect()
|
||||
self._sender.disconnect()
|
||||
if getattr(self, '_updates_handle', None):
|
||||
await self._updates_handle
|
||||
self._updates_handle
|
||||
|
||||
def __del__(self):
|
||||
if not self.is_connected() or self.loop.is_closed():
|
||||
|
@ -324,20 +324,20 @@ class TelegramBaseClient(abc.ABC):
|
|||
else:
|
||||
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.
|
||||
"""
|
||||
__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)
|
||||
# auth_key's are associated with a server, which has now changed
|
||||
# so it's not valid anymore. Set to None to force recreating it.
|
||||
self.session.auth_key = self._sender.state.auth_key = None
|
||||
self.session.save()
|
||||
await self._disconnect()
|
||||
return await self.connect()
|
||||
self._disconnect()
|
||||
return self.connect()
|
||||
|
||||
def _auth_key_callback(self, auth_key):
|
||||
"""
|
||||
|
@ -352,14 +352,14 @@ class TelegramBaseClient(abc.ABC):
|
|||
|
||||
# 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'"""
|
||||
cls = self.__class__
|
||||
if not cls._config:
|
||||
cls._config = await self(functions.help.GetConfigRequest())
|
||||
cls._config = self(functions.help.GetConfigRequest())
|
||||
|
||||
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:
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
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
|
||||
# for clearly showing how to export the authorization
|
||||
auth = self._exported_auths.get(dc_id)
|
||||
dc = await self._get_dc(dc_id)
|
||||
dc = self._get_dc(dc_id)
|
||||
state = MTProtoState(auth)
|
||||
# Can't reuse self._sender._connection as it has its own seqno.
|
||||
#
|
||||
# If one were to do that, Telegram would reset the connection
|
||||
# with no further clues.
|
||||
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:
|
||||
__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(
|
||||
id=auth.id, bytes=auth.bytes
|
||||
))
|
||||
await sender.send(req)
|
||||
sender.send(req)
|
||||
self._exported_auths[dc_id] = sender.state.auth_key
|
||||
|
||||
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"""
|
||||
# TODO Implement
|
||||
raise NotImplementedError
|
||||
session = self._exported_sessions.get(cdn_redirect.dc_id)
|
||||
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.set_dc(dc.id, dc.ip_address, dc.port)
|
||||
self._exported_sessions[cdn_redirect.dc_id] = session
|
||||
|
@ -458,7 +458,7 @@ class TelegramBaseClient(abc.ABC):
|
|||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _handle_auto_reconnect(self):
|
||||
def _handle_auto_reconnect(self):
|
||||
raise NotImplementedError
|
||||
|
||||
# endregion
|
||||
|
|
|
@ -16,11 +16,11 @@ class UpdateMethods(UserMethods):
|
|||
|
||||
# region Public methods
|
||||
|
||||
async def _run_until_disconnected(self):
|
||||
def _run_until_disconnected(self):
|
||||
try:
|
||||
await self.disconnected
|
||||
self.disconnected
|
||||
except KeyboardInterrupt:
|
||||
await self.disconnect()
|
||||
self.disconnect()
|
||||
|
||||
def run_until_disconnected(self):
|
||||
"""
|
||||
|
@ -30,7 +30,7 @@ class UpdateMethods(UserMethods):
|
|||
to ``except`` it on your own code.
|
||||
|
||||
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():
|
||||
return self._run_until_disconnected()
|
||||
|
@ -52,7 +52,7 @@ class UpdateMethods(UserMethods):
|
|||
>>> client = TelegramClient(...)
|
||||
>>>
|
||||
>>> @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]
|
||||
|
||||
async def catch_up(self):
|
||||
def catch_up(self):
|
||||
state = self.session.get_update_state(0)
|
||||
if not state or not state.pts:
|
||||
return
|
||||
|
@ -128,7 +128,7 @@ class UpdateMethods(UserMethods):
|
|||
self.session.catching_up = True
|
||||
try:
|
||||
while True:
|
||||
d = await self(functions.updates.GetDifferenceRequest(
|
||||
d = self(functions.updates.GetDifferenceRequest(
|
||||
state.pts, state.date, state.qts))
|
||||
if isinstance(d, types.updates.DifferenceEmpty):
|
||||
state.date = d.date
|
||||
|
@ -193,12 +193,12 @@ class UpdateMethods(UserMethods):
|
|||
|
||||
# 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"
|
||||
rnd = lambda: random.randrange(-2**63, 2**63)
|
||||
while self.is_connected():
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.wait_for(
|
||||
self.disconnected, timeout=60, loop=self._loop
|
||||
)
|
||||
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
|
||||
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
|
||||
# long without being logged in...?
|
||||
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._event_resolve_lock.locked():
|
||||
async with self._event_resolve_lock:
|
||||
with self._event_resolve_lock:
|
||||
pass
|
||||
else:
|
||||
async with self._event_resolve_lock:
|
||||
with self._event_resolve_lock:
|
||||
for event in self._events_pending_resolve:
|
||||
await event.resolve(self)
|
||||
event.resolve(self)
|
||||
|
||||
self._events_pending_resolve.clear()
|
||||
|
||||
|
@ -252,7 +252,7 @@ class UpdateMethods(UserMethods):
|
|||
|
||||
event.original_update = update
|
||||
try:
|
||||
await callback(event)
|
||||
callback(event)
|
||||
except events.StopPropagation:
|
||||
__log__.debug(
|
||||
"Event handler '{}' stopped chain of "
|
||||
|
@ -265,12 +265,12 @@ class UpdateMethods(UserMethods):
|
|||
__log__.exception('Unhandled exception on {}'
|
||||
.format(callback.__name__))
|
||||
|
||||
async def _handle_auto_reconnect(self):
|
||||
def _handle_auto_reconnect(self):
|
||||
# Upon reconnection, we want to send getState
|
||||
# for Telegram to keep sending us updates.
|
||||
try:
|
||||
__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)
|
||||
except errors.RPCError as e:
|
||||
__log__.info('Failed to get current state: %r', e)
|
||||
|
|
|
@ -26,7 +26,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
|
||||
# region Public methods
|
||||
|
||||
async def send_file(
|
||||
def send_file(
|
||||
self, entity, file, *, caption='', force_document=False,
|
||||
progress_callback=None, reply_to=None, attributes=None,
|
||||
thumb=None, allow_cache=True, parse_mode=utils.Default,
|
||||
|
@ -122,7 +122,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
|
||||
result = []
|
||||
while images:
|
||||
result += await self._send_album(
|
||||
result += self._send_album(
|
||||
entity, images[:10], caption=caption,
|
||||
progress_callback=progress_callback, reply_to=reply_to,
|
||||
parse_mode=parse_mode
|
||||
|
@ -130,7 +130,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
images = images[10:]
|
||||
|
||||
for x in documents:
|
||||
result.append(await self.send_file(
|
||||
result.append(self.send_file(
|
||||
entity, x, allow_cache=allow_cache,
|
||||
caption=caption, force_document=force_document,
|
||||
progress_callback=progress_callback, reply_to=reply_to,
|
||||
|
@ -140,7 +140,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
|
||||
return result
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
entity = self.get_input_entity(entity)
|
||||
reply_to = utils.get_message_id(reply_to)
|
||||
|
||||
# Not document since it's subject to change.
|
||||
|
@ -149,9 +149,9 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
msg_entities = kwargs['entities']
|
||||
else:
|
||||
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,
|
||||
progress_callback=progress_callback,
|
||||
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,
|
||||
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)
|
||||
|
||||
return msg
|
||||
|
||||
async def _send_album(self, entity, files, caption='',
|
||||
def _send_album(self, entity, files, caption='',
|
||||
progress_callback=None, reply_to=None,
|
||||
parse_mode=utils.Default):
|
||||
"""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
|
||||
# as different messages (not inside the album), and the logic to set
|
||||
# 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):
|
||||
caption = (caption,)
|
||||
|
||||
captions = []
|
||||
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)
|
||||
|
||||
|
@ -194,9 +194,9 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
media = []
|
||||
for file in files:
|
||||
# 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):
|
||||
r = await self(functions.messages.UploadMediaRequest(
|
||||
r = self(functions.messages.UploadMediaRequest(
|
||||
entity, media=types.InputMediaUploadedPhoto(fh)
|
||||
))
|
||||
input_photo = utils.get_input_photo(r.photo)
|
||||
|
@ -211,7 +211,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
entities=msg_entities))
|
||||
|
||||
# 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
|
||||
))
|
||||
return [
|
||||
|
@ -220,7 +220,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
if isinstance(update, types.UpdateMessageID)
|
||||
]
|
||||
|
||||
async def upload_file(
|
||||
def upload_file(
|
||||
self, file, *, part_size_kb=None, file_name=None, use_cache=None,
|
||||
progress_callback=None):
|
||||
"""
|
||||
|
@ -337,7 +337,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
request = functions.upload.SaveFilePartRequest(
|
||||
file_id, part_index, part)
|
||||
|
||||
result = await self(request)
|
||||
result = self(request)
|
||||
if result:
|
||||
__log__.debug('Uploaded %d/%d', part_index + 1,
|
||||
part_count)
|
||||
|
@ -356,7 +356,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
|
||||
# endregion
|
||||
|
||||
async def _file_to_media(
|
||||
def _file_to_media(
|
||||
self, file, force_document=False,
|
||||
progress_callback=None, attributes=None, thumb=None,
|
||||
allow_cache=True, voice_note=False, video_note=False):
|
||||
|
@ -387,7 +387,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
else:
|
||||
media = types.InputMediaDocumentExternal(file)
|
||||
else:
|
||||
file_handle = await self.upload_file(
|
||||
file_handle = self.upload_file(
|
||||
file, progress_callback=progress_callback,
|
||||
use_cache=use_cache if allow_cache else None
|
||||
)
|
||||
|
@ -476,7 +476,7 @@ class UploadMethods(MessageParseMethods, UserMethods):
|
|||
|
||||
input_kw = {}
|
||||
if thumb:
|
||||
input_kw['thumb'] = await self.upload_file(thumb)
|
||||
input_kw['thumb'] = self.upload_file(thumb)
|
||||
|
||||
media = types.InputMediaUploadedDocument(
|
||||
file=file_handle,
|
||||
|
|
|
@ -12,11 +12,11 @@ _NOT_A_REQUEST = TypeError('You can only invoke requests, not types!')
|
|||
|
||||
|
||||
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,)):
|
||||
if not isinstance(r, TLRequest):
|
||||
raise _NOT_A_REQUEST
|
||||
await r.resolve(self, utils)
|
||||
r.resolve(self, utils)
|
||||
|
||||
self._last_request = time.time()
|
||||
for _ in range(self._request_retries):
|
||||
|
@ -25,12 +25,12 @@ class UserMethods(TelegramBaseClient):
|
|||
if isinstance(future, list):
|
||||
results = []
|
||||
for f in future:
|
||||
result = await f
|
||||
result = f
|
||||
self.session.process_entities(result)
|
||||
results.append(result)
|
||||
return results
|
||||
else:
|
||||
result = await future
|
||||
result = future
|
||||
self.session.process_entities(result)
|
||||
return result
|
||||
except (errors.ServerError, errors.RpcCallFailError) as e:
|
||||
|
@ -39,7 +39,7 @@ class UserMethods(TelegramBaseClient):
|
|||
except (errors.FloodWaitError, errors.FloodTestPhoneWaitError) as e:
|
||||
if e.seconds <= self.flood_sleep_threshold:
|
||||
__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:
|
||||
raise
|
||||
except (errors.PhoneMigrateError, errors.NetworkMigrateError,
|
||||
|
@ -48,15 +48,15 @@ class UserMethods(TelegramBaseClient):
|
|||
should_raise = isinstance(e, (
|
||||
errors.PhoneMigrateError, errors.NetworkMigrateError
|
||||
))
|
||||
if should_raise and await self.is_user_authorized():
|
||||
if should_raise and self.is_user_authorized():
|
||||
raise
|
||||
await self._switch_dc(e.new_dc)
|
||||
self._switch_dc(e.new_dc)
|
||||
|
||||
raise ValueError('Number of retries reached 0')
|
||||
|
||||
# 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,
|
||||
or None if the request fails (hence, not authenticated).
|
||||
|
@ -74,7 +74,7 @@ class UserMethods(TelegramBaseClient):
|
|||
return self._self_input_peer
|
||||
|
||||
try:
|
||||
me = (await self(
|
||||
me = (self(
|
||||
functions.users.GetUsersRequest([types.InputUserSelf()])))[0]
|
||||
|
||||
if not self._self_input_peer:
|
||||
|
@ -86,7 +86,7 @@ class UserMethods(TelegramBaseClient):
|
|||
except errors.UnauthorizedError:
|
||||
return None
|
||||
|
||||
async def is_user_authorized(self):
|
||||
def is_user_authorized(self):
|
||||
"""
|
||||
Returns ``True`` if the user is authorized.
|
||||
"""
|
||||
|
@ -94,12 +94,12 @@ class UserMethods(TelegramBaseClient):
|
|||
return True
|
||||
|
||||
try:
|
||||
self._state = await self(functions.updates.GetStateRequest())
|
||||
self._state = self(functions.updates.GetStateRequest())
|
||||
return True
|
||||
except errors.RPCError:
|
||||
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`
|
||||
or :tl:`Channel`. You can also pass a list or iterable of entities,
|
||||
|
@ -144,7 +144,7 @@ class UserMethods(TelegramBaseClient):
|
|||
if isinstance(x, str):
|
||||
inputs.append(x)
|
||||
else:
|
||||
inputs.append(await self.get_input_entity(x))
|
||||
inputs.append(self.get_input_entity(x))
|
||||
|
||||
users = [x for x in inputs
|
||||
if isinstance(x, (types.InputPeerUser, types.InputPeerSelf))]
|
||||
|
@ -157,13 +157,13 @@ class UserMethods(TelegramBaseClient):
|
|||
tmp = []
|
||||
while users:
|
||||
curr, users = users[:200], users[200:]
|
||||
tmp.extend(await self(functions.users.GetUsersRequest(curr)))
|
||||
tmp.extend(self(functions.users.GetUsersRequest(curr)))
|
||||
users = tmp
|
||||
if chats: # TODO Handle chats slice?
|
||||
chats = (await self(
|
||||
chats = (self(
|
||||
functions.messages.GetChatsRequest(chats))).chats
|
||||
if channels:
|
||||
channels = (await self(
|
||||
channels = (self(
|
||||
functions.channels.GetChannelsRequest(channels))).chats
|
||||
|
||||
# Merge users, chats and channels into a single dictionary
|
||||
|
@ -179,7 +179,7 @@ class UserMethods(TelegramBaseClient):
|
|||
result = []
|
||||
for x in inputs:
|
||||
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):
|
||||
result.append(id_entity[utils.get_peer_id(x)])
|
||||
else:
|
||||
|
@ -190,7 +190,7 @@ class UserMethods(TelegramBaseClient):
|
|||
|
||||
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
|
||||
use this kind of :tl:`InputPeer`, so this is the most suitable call
|
||||
|
@ -260,7 +260,7 @@ class UserMethods(TelegramBaseClient):
|
|||
|
||||
if isinstance(peer, str):
|
||||
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)
|
||||
or peer.SUBCLASS_OF_ID != 0x2d45687):
|
||||
|
@ -280,7 +280,7 @@ class UserMethods(TelegramBaseClient):
|
|||
|
||||
# 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
|
||||
an username, and processes all the found entities on the session.
|
||||
|
@ -294,14 +294,14 @@ class UserMethods(TelegramBaseClient):
|
|||
"""
|
||||
phone = utils.parse_phone(string)
|
||||
if phone:
|
||||
for user in (await self(
|
||||
for user in (self(
|
||||
functions.contacts.GetContactsRequest(0))).users:
|
||||
if user.phone == phone:
|
||||
return user
|
||||
else:
|
||||
username, is_join_chat = utils.parse_username(string)
|
||||
if is_join_chat:
|
||||
invite = await self(
|
||||
invite = self(
|
||||
functions.messages.CheckChatInviteRequest(username))
|
||||
|
||||
if isinstance(invite, types.ChatInvite):
|
||||
|
@ -313,10 +313,10 @@ class UserMethods(TelegramBaseClient):
|
|||
return invite.chat
|
||||
elif username:
|
||||
if username in ('me', 'self'):
|
||||
return await self.get_me()
|
||||
return self.get_me()
|
||||
|
||||
try:
|
||||
result = await self(
|
||||
result = self(
|
||||
functions.contacts.ResolveUsernameRequest(username))
|
||||
except errors.UsernameNotOccupiedError as e:
|
||||
raise ValueError('No user has "{}" as username'
|
||||
|
@ -328,7 +328,7 @@ class UserMethods(TelegramBaseClient):
|
|||
return entity
|
||||
try:
|
||||
# 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))
|
||||
except ValueError:
|
||||
pass
|
||||
|
|
|
@ -30,7 +30,7 @@ class CdnDecrypter:
|
|||
self.cdn_file_hashes = cdn_file_hashes
|
||||
|
||||
@staticmethod
|
||||
async def prepare_decrypter(client, cdn_client, cdn_redirect):
|
||||
def prepare_decrypter(client, cdn_client, cdn_redirect):
|
||||
"""
|
||||
Prepares a new CDN decrypter.
|
||||
|
||||
|
@ -52,14 +52,14 @@ class CdnDecrypter:
|
|||
cdn_aes, cdn_redirect.cdn_file_hashes
|
||||
)
|
||||
|
||||
cdn_file = await cdn_client(GetCdnFileRequest(
|
||||
cdn_file = cdn_client(GetCdnFileRequest(
|
||||
file_token=cdn_redirect.file_token,
|
||||
offset=cdn_redirect.cdn_file_hashes[0].offset,
|
||||
limit=cdn_redirect.cdn_file_hashes[0].limit
|
||||
))
|
||||
if isinstance(cdn_file, CdnFileReuploadNeeded):
|
||||
# We need to use the original client here
|
||||
await client(ReuploadCdnFileRequest(
|
||||
client(ReuploadCdnFileRequest(
|
||||
file_token=cdn_redirect.file_token,
|
||||
request_token=cdn_file.request_token
|
||||
))
|
||||
|
|
|
@ -166,16 +166,16 @@ class ChatAction(EventBuilder):
|
|||
self.action_message = custom.Message(
|
||||
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
|
||||
`telethon.telegram_client.TelegramClient.send_message` with
|
||||
``entity`` already set.
|
||||
"""
|
||||
return await self._client.send_message(
|
||||
await self.get_input_chat(), *args, **kwargs)
|
||||
return self._client.send_message(
|
||||
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
|
||||
`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.
|
||||
"""
|
||||
if not self.action_message:
|
||||
return await self.respond(*args, **kwargs)
|
||||
return self.respond(*args, **kwargs)
|
||||
|
||||
kwargs['reply_to'] = self.action_message.id
|
||||
return await self._client.send_message(
|
||||
await self.get_input_chat(), *args, **kwargs)
|
||||
return self._client.send_message(
|
||||
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
|
||||
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:
|
||||
return
|
||||
|
||||
return await self._client.delete_messages(
|
||||
await self.get_input_chat(), [self.action_message],
|
||||
return self._client.delete_messages(
|
||||
self.get_input_chat(), [self.action_message],
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
async def get_pinned_message(self):
|
||||
def get_pinned_message(self):
|
||||
"""
|
||||
If ``new_pin`` is ``True``, this returns the
|
||||
`telethon.tl.custom.message.Message` object that was pinned.
|
||||
|
@ -217,8 +217,8 @@ class ChatAction(EventBuilder):
|
|||
return None
|
||||
|
||||
if isinstance(self._pinned_message, int)\
|
||||
and await self.get_input_chat():
|
||||
r = await self._client(functions.channels.GetMessagesRequest(
|
||||
and self.get_input_chat():
|
||||
r = self._client(functions.channels.GetMessagesRequest(
|
||||
self._input_chat, [self._pinned_message]
|
||||
))
|
||||
try:
|
||||
|
@ -245,12 +245,12 @@ class ChatAction(EventBuilder):
|
|||
|
||||
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.
|
||||
"""
|
||||
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
|
||||
|
||||
|
@ -266,12 +266,12 @@ class ChatAction(EventBuilder):
|
|||
|
||||
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.
|
||||
"""
|
||||
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
|
||||
|
||||
|
@ -286,11 +286,11 @@ class ChatAction(EventBuilder):
|
|||
if self.users:
|
||||
return self._users[0]
|
||||
|
||||
async def get_user(self):
|
||||
def get_user(self):
|
||||
"""
|
||||
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]
|
||||
|
||||
def input_user(self):
|
||||
|
@ -300,11 +300,11 @@ class ChatAction(EventBuilder):
|
|||
if self.input_users:
|
||||
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.
|
||||
"""
|
||||
if self.input_users or await self.get_input_users():
|
||||
if self.input_users or self.get_input_users():
|
||||
return self._input_users[0]
|
||||
|
||||
@property
|
||||
|
@ -335,7 +335,7 @@ class ChatAction(EventBuilder):
|
|||
|
||||
return self._users
|
||||
|
||||
async def get_users(self):
|
||||
def get_users(self):
|
||||
"""
|
||||
Returns `users` but will make an API call if necessary.
|
||||
"""
|
||||
|
@ -352,7 +352,7 @@ class ChatAction(EventBuilder):
|
|||
missing.append(peer)
|
||||
|
||||
try:
|
||||
missing = await self._client.get_entity(missing)
|
||||
missing = self._client.get_entity(missing)
|
||||
except (TypeError, ValueError):
|
||||
missing = []
|
||||
|
||||
|
@ -376,7 +376,7 @@ class ChatAction(EventBuilder):
|
|||
pass
|
||||
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.
|
||||
"""
|
||||
|
|
|
@ -5,7 +5,7 @@ from .. import utils
|
|||
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."""
|
||||
if chats is None:
|
||||
return None
|
||||
|
@ -28,9 +28,9 @@ async def _into_id_set(client, chats):
|
|||
# 0x2d45687 == crc32(b'Peer')
|
||||
result.add(utils.get_peer_id(chat))
|
||||
else:
|
||||
chat = await client.get_input_entity(chat)
|
||||
chat = client.get_input_entity(chat)
|
||||
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))
|
||||
|
||||
return result
|
||||
|
@ -60,10 +60,10 @@ class EventBuilder(abc.ABC):
|
|||
def build(self, update):
|
||||
"""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"""
|
||||
self.chats = await _into_id_set(client, self.chats)
|
||||
self._self_id = (await client.get_me(input_peer=True)).user_id
|
||||
self.chats = _into_id_set(client, self.chats)
|
||||
self._self_id = (client.get_me(input_peer=True)).user_id
|
||||
|
||||
def _filter_event(self, event):
|
||||
"""
|
||||
|
@ -130,7 +130,7 @@ class EventCommon(abc.ABC):
|
|||
|
||||
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
|
||||
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:
|
||||
ch = isinstance(self._chat_peer, types.PeerChannel)
|
||||
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)
|
||||
self._chat = msg._chat
|
||||
self._input_chat = msg._input_chat
|
||||
else:
|
||||
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:
|
||||
self._chat = d.entity
|
||||
self._input_chat = d.input_entity
|
||||
|
@ -179,15 +179,15 @@ class EventCommon(abc.ABC):
|
|||
|
||||
return self._chat
|
||||
|
||||
async def get_chat(self):
|
||||
def get_chat(self):
|
||||
"""
|
||||
Returns `chat`, but will make an API call to find the
|
||||
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:
|
||||
self._chat =\
|
||||
await self._client.get_entity(self._input_chat)
|
||||
self._client.get_entity(self._input_chat)
|
||||
except ValueError:
|
||||
pass
|
||||
return self._chat
|
||||
|
|
|
@ -88,7 +88,7 @@ class MessageRead(EventBuilder):
|
|||
"""
|
||||
return self._message_ids
|
||||
|
||||
async def get_messages(self):
|
||||
def get_messages(self):
|
||||
"""
|
||||
Returns the list of `telethon.tl.custom.message.Message`
|
||||
**which contents'** were read.
|
||||
|
@ -97,11 +97,11 @@ class MessageRead(EventBuilder):
|
|||
was read instead checking if it's in here.
|
||||
"""
|
||||
if self._messages is None:
|
||||
chat = await self.get_input_chat()
|
||||
chat = self.get_input_chat()
|
||||
if not chat:
|
||||
self._messages = []
|
||||
else:
|
||||
self._messages = await self._client.get_messages(
|
||||
self._messages = self._client.get_messages(
|
||||
chat, ids=self._message_ids)
|
||||
|
||||
return self._messages
|
||||
|
|
|
@ -71,9 +71,9 @@ class NewMessage(EventBuilder):
|
|||
self.from_users, self.forwards, self.from_users
|
||||
))
|
||||
|
||||
async def resolve(self, client):
|
||||
await super().resolve(client)
|
||||
self.from_users = await _into_id_set(client, self.from_users)
|
||||
def resolve(self, client):
|
||||
super().resolve(client)
|
||||
self.from_users = _into_id_set(client, self.from_users)
|
||||
|
||||
def build(self, update):
|
||||
if isinstance(update,
|
||||
|
@ -179,7 +179,7 @@ class NewMessage(EventBuilder):
|
|||
>>> client = TelegramClient(...)
|
||||
>>>
|
||||
>>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!'))
|
||||
... async def handler(event):
|
||||
... def handler(event):
|
||||
... # In this case, the result is a ``Match`` object
|
||||
... # since the ``str`` pattern was converted into
|
||||
... # the ``re.compile(pattern).match`` function.
|
||||
|
|
|
@ -22,7 +22,7 @@ class Raw(EventBuilder):
|
|||
assert all(isinstance(x, type) for x in types)
|
||||
self.types = tuple(types)
|
||||
|
||||
async def resolve(self, client):
|
||||
def resolve(self, client):
|
||||
pass
|
||||
|
||||
def build(self, update):
|
||||
|
|
|
@ -152,18 +152,18 @@ class UserUpdate(EventBuilder):
|
|||
"""Alias for `chat` (conversation)."""
|
||||
return self.chat
|
||||
|
||||
async def get_user(self):
|
||||
def get_user(self):
|
||||
"""Alias for `get_chat` (conversation)."""
|
||||
return await self.get_chat()
|
||||
return self.get_chat()
|
||||
|
||||
@property
|
||||
def input_user(self):
|
||||
"""Alias for `input_chat`."""
|
||||
return self.input_chat
|
||||
|
||||
async def get_input_user(self):
|
||||
def get_input_user(self):
|
||||
"""Alias for `get_input_chat`."""
|
||||
return await self.get_input_chat()
|
||||
return self.get_input_chat()
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
|
|
|
@ -71,7 +71,7 @@ class TcpClient:
|
|||
s.setblocking(False)
|
||||
return s
|
||||
|
||||
async def connect(self, ip, port):
|
||||
def connect(self, ip, port):
|
||||
"""
|
||||
Tries connecting to IP:port unless an OSError is raised.
|
||||
|
||||
|
@ -88,7 +88,7 @@ class TcpClient:
|
|||
if self._socket is None:
|
||||
self._socket = self._create_socket(mode, self.proxy)
|
||||
|
||||
await asyncio.wait_for(
|
||||
asyncio.wait_for(
|
||||
self._loop.sock_connect(self._socket, address),
|
||||
timeout=self.timeout,
|
||||
loop=self._loop
|
||||
|
@ -122,12 +122,12 @@ class TcpClient:
|
|||
if 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
|
||||
the socket is closed or `self.timeout` expires.
|
||||
"""
|
||||
done, running = await asyncio.wait(
|
||||
done, running = asyncio.wait(
|
||||
[coro, self._closed.wait()],
|
||||
timeout=self.timeout,
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
|
@ -141,7 +141,7 @@ class TcpClient:
|
|||
raise asyncio.TimeoutError()
|
||||
return done.pop().result()
|
||||
|
||||
async def write(self, data):
|
||||
def write(self, data):
|
||||
"""
|
||||
Writes (sends) the specified bytes to the connected peer.
|
||||
:param data: the data to send.
|
||||
|
@ -150,14 +150,14 @@ class TcpClient:
|
|||
raise ConnectionResetError('Not connected')
|
||||
|
||||
try:
|
||||
await self._wait_timeout_or_close(self.sock_sendall(data))
|
||||
self._wait_timeout_or_close(self.sock_sendall(data))
|
||||
except OSError as e:
|
||||
if e.errno in CONN_RESET_ERRNOS:
|
||||
raise ConnectionResetError() from e
|
||||
else:
|
||||
raise
|
||||
|
||||
async def read(self, size):
|
||||
def read(self, size):
|
||||
"""
|
||||
Reads (receives) a whole block of size bytes from the connected peer.
|
||||
|
||||
|
@ -171,7 +171,7 @@ class TcpClient:
|
|||
bytes_left = size
|
||||
while bytes_left != 0:
|
||||
try:
|
||||
partial = await self._wait_timeout_or_close(
|
||||
partial = self._wait_timeout_or_close(
|
||||
self.sock_recv(bytes_left)
|
||||
)
|
||||
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.
|
||||
|
||||
|
@ -28,7 +28,7 @@ async def do_authentication(sender):
|
|||
"""
|
||||
# Step 1 sending: PQ Request, endianness doesn't matter since it's random
|
||||
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
|
||||
|
||||
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,
|
||||
server_nonce=res_pq.server_nonce,
|
||||
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)
|
||||
|
||||
# Prepare Set client DH params
|
||||
dh_gen = await sender.send(SetClientDHParamsRequest(
|
||||
dh_gen = sender.send(SetClientDHParamsRequest(
|
||||
nonce=res_pq.nonce,
|
||||
server_nonce=res_pq.server_nonce,
|
||||
encrypted_data=client_dh_encrypted,
|
||||
|
|
|
@ -33,7 +33,7 @@ class Connection(abc.ABC):
|
|||
self._timeout = timeout
|
||||
|
||||
@abc.abstractmethod
|
||||
async def connect(self, ip, port):
|
||||
def connect(self, ip, port):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@ -51,7 +51,7 @@ class Connection(abc.ABC):
|
|||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
async def close(self):
|
||||
def close(self):
|
||||
"""Closes the connection."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
@ -64,11 +64,11 @@ class Connection(abc.ABC):
|
|||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
async def recv(self):
|
||||
def recv(self):
|
||||
"""Receives and unpacks a message"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
async def send(self, message):
|
||||
def send(self, message):
|
||||
"""Encapsulates and sends the given message"""
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -9,23 +9,23 @@ class ConnectionTcpAbridged(ConnectionTcpFull):
|
|||
only require 1 byte if the packet length is less than
|
||||
508 bytes (127 << 2, which is very common).
|
||||
"""
|
||||
async def connect(self, ip, port):
|
||||
result = await super().connect(ip, port)
|
||||
await self.conn.write(b'\xef')
|
||||
def connect(self, ip, port):
|
||||
result = super().connect(ip, port)
|
||||
self.conn.write(b'\xef')
|
||||
return result
|
||||
|
||||
async def recv(self):
|
||||
length = struct.unpack('<B', await self.read(1))[0]
|
||||
def recv(self):
|
||||
length = struct.unpack('<B', self.read(1))[0]
|
||||
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
|
||||
if length < 127:
|
||||
length = struct.pack('B', length)
|
||||
else:
|
||||
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.write = self.conn.write
|
||||
|
||||
async def connect(self, ip, port):
|
||||
def connect(self, ip, port):
|
||||
try:
|
||||
await self.conn.connect(ip, port)
|
||||
self.conn.connect(ip, port)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EISCONN:
|
||||
return # Already connected, no need to re-set everything up
|
||||
|
@ -38,13 +38,13 @@ class ConnectionTcpFull(Connection):
|
|||
def is_connected(self):
|
||||
return self.conn.is_connected
|
||||
|
||||
async def close(self):
|
||||
def close(self):
|
||||
self.conn.close()
|
||||
|
||||
async def recv(self):
|
||||
packet_len_seq = await self.read(8) # 4 and 4
|
||||
def recv(self):
|
||||
packet_len_seq = self.read(8) # 4 and 4
|
||||
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]
|
||||
body = body[:-4]
|
||||
|
||||
|
@ -54,11 +54,11 @@ class ConnectionTcpFull(Connection):
|
|||
|
||||
return body
|
||||
|
||||
async def send(self, message):
|
||||
def send(self, message):
|
||||
# https://core.telegram.org/mtproto#tcp-transport
|
||||
# total length, sequence number, packet and checksum (CRC32)
|
||||
length = len(message) + 12
|
||||
data = struct.pack('<ii', length, self._send_counter) + message
|
||||
crc = struct.pack('<I', crc32(data))
|
||||
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`.
|
||||
Always sends 4 extra bytes for the packet length.
|
||||
"""
|
||||
async def connect(self, ip, port):
|
||||
result = await super().connect(ip, port)
|
||||
await self.conn.write(b'\xee\xee\xee\xee')
|
||||
def connect(self, ip, port):
|
||||
result = super().connect(ip, port)
|
||||
self.conn.write(b'\xee\xee\xee\xee')
|
||||
return result
|
||||
|
||||
async def recv(self):
|
||||
return await self.read(struct.unpack('<i', await self.read(4))[0])
|
||||
def recv(self):
|
||||
return self.read(struct.unpack('<i', self.read(4))[0])
|
||||
|
||||
async def send(self, message):
|
||||
await self.write(struct.pack('<i', len(message)) + message)
|
||||
def send(self, 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.write = lambda d: self.conn.write(self._aes_encrypt.encrypt(d))
|
||||
|
||||
async def connect(self, ip, port):
|
||||
result = await ConnectionTcpFull.connect(self, ip, port)
|
||||
def connect(self, ip, port):
|
||||
result = ConnectionTcpFull.connect(self, ip, port)
|
||||
# Obfuscated messages secrets cannot start with any of these
|
||||
keywords = (b'PVrG', b'GET ', b'POST', b'\xee' * 4)
|
||||
while True:
|
||||
|
@ -42,5 +42,5 @@ class ConnectionTcpObfuscated(ConnectionTcpAbridged):
|
|||
self._aes_decrypt = AESModeCTR(decrypt_key, decrypt_iv)
|
||||
|
||||
random[56:64] = self._aes_encrypt.encrypt(bytes(random))[56:64]
|
||||
await self.conn.write(bytes(random))
|
||||
self.conn.write(bytes(random))
|
||||
return result
|
||||
|
|
|
@ -23,17 +23,17 @@ class MTProtoPlainSender:
|
|||
self._state = MTProtoState(auth_key=None)
|
||||
self._connection = connection
|
||||
|
||||
async def send(self, request):
|
||||
def send(self, request):
|
||||
"""
|
||||
Sends and receives the result for the given request.
|
||||
"""
|
||||
body = bytes(request)
|
||||
msg_id = self._state._get_new_msg_id()
|
||||
await self._connection.send(
|
||||
self._connection.send(
|
||||
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
|
||||
# Broken authorization, must reset the auth key
|
||||
raise BrokenAuthKeyError()
|
||||
|
|
|
@ -110,7 +110,7 @@ class MTProtoSender:
|
|||
|
||||
# Public API
|
||||
|
||||
async def connect(self, ip, port):
|
||||
def connect(self, ip, port):
|
||||
"""
|
||||
Connects to the specified ``ip:port``, and generates a new
|
||||
authorization key for the `MTProtoSender.session` if it does
|
||||
|
@ -123,12 +123,12 @@ class MTProtoSender:
|
|||
self._ip = ip
|
||||
self._port = port
|
||||
self._user_connected = True
|
||||
await self._connect()
|
||||
self._connect()
|
||||
|
||||
def is_connected(self):
|
||||
return self._user_connected
|
||||
|
||||
async def disconnect(self):
|
||||
def disconnect(self):
|
||||
"""
|
||||
Cleanly disconnects the instance from the network, cancels
|
||||
all pending requests, and closes the send and receive loops.
|
||||
|
@ -137,14 +137,14 @@ class MTProtoSender:
|
|||
__log__.info('User is already disconnected!')
|
||||
return
|
||||
|
||||
await self._disconnect()
|
||||
self._disconnect()
|
||||
|
||||
async def _disconnect(self, error=None):
|
||||
def _disconnect(self, error=None):
|
||||
__log__.info('Disconnecting from {}...'.format(self._ip))
|
||||
self._user_connected = False
|
||||
try:
|
||||
__log__.debug('Closing current connection...')
|
||||
await self._connection.close()
|
||||
self._connection.close()
|
||||
finally:
|
||||
__log__.debug('Cancelling {} pending message(s)...'
|
||||
.format(len(self._pending_messages)))
|
||||
|
@ -183,11 +183,11 @@ class MTProtoSender:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
async def method():
|
||||
def method():
|
||||
# Sending (enqueued for the send loop)
|
||||
future = sender.send(request)
|
||||
# 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
|
||||
any point, and it can send other items while one waits for it.
|
||||
|
@ -196,7 +196,7 @@ class MTProtoSender:
|
|||
would otherwise work.
|
||||
|
||||
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:
|
||||
raise ConnectionError('Cannot send requests while disconnected')
|
||||
|
@ -230,7 +230,7 @@ class MTProtoSender:
|
|||
|
||||
# Private methods
|
||||
|
||||
async def _connect(self):
|
||||
def _connect(self):
|
||||
"""
|
||||
Performs the actual connection, retrying, generating the
|
||||
authorization key if necessary, and starting the send and
|
||||
|
@ -240,7 +240,7 @@ class MTProtoSender:
|
|||
for retry in range(1, self._retries + 1):
|
||||
try:
|
||||
__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:
|
||||
__log__.warning('Attempt {} at connecting failed: {}: {}'
|
||||
.format(retry, type(e).__name__, e))
|
||||
|
@ -257,7 +257,7 @@ class MTProtoSender:
|
|||
try:
|
||||
__log__.debug('New auth_key attempt {}...'.format(retry))
|
||||
self.state.auth_key, self.state.time_offset =\
|
||||
await authenticator.do_authentication(plain)
|
||||
authenticator.do_authentication(plain)
|
||||
|
||||
if self._auth_key_callback:
|
||||
self._auth_key_callback(self.state.auth_key)
|
||||
|
@ -269,7 +269,7 @@ class MTProtoSender:
|
|||
else:
|
||||
e = ConnectionError('auth_key generation failed {} times'
|
||||
.format(self._retries))
|
||||
await self._disconnect(error=e)
|
||||
self._disconnect(error=e)
|
||||
raise e
|
||||
|
||||
__log__.debug('Starting send loop')
|
||||
|
@ -283,7 +283,7 @@ class MTProtoSender:
|
|||
self._disconnected = asyncio.Future()
|
||||
__log__.info('Connection to {} complete!'.format(self._ip))
|
||||
|
||||
async def _reconnect(self):
|
||||
def _reconnect(self):
|
||||
"""
|
||||
Cleanly disconnects and then reconnects.
|
||||
"""
|
||||
|
@ -291,20 +291,20 @@ class MTProtoSender:
|
|||
self._send_queue.put_nowait(_reconnect_sentinel)
|
||||
|
||||
__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...')
|
||||
await self._recv_loop_handle
|
||||
self._recv_loop_handle
|
||||
|
||||
__log__.debug('Closing current connection...')
|
||||
await self._connection.close()
|
||||
self._connection.close()
|
||||
|
||||
self._reconnecting = False
|
||||
|
||||
retries = self._retries if self._auto_reconnect else 0
|
||||
for retry in range(1, retries + 1):
|
||||
try:
|
||||
await self._connect()
|
||||
self._connect()
|
||||
for m in self._pending_messages.values():
|
||||
self._send_queue.put_nowait(m)
|
||||
|
||||
|
@ -316,7 +316,7 @@ class MTProtoSender:
|
|||
__log__.info('Failed reconnection retry %d/%d', retry, retries)
|
||||
else:
|
||||
__log__.error('Failed to reconnect automatically.')
|
||||
await self._disconnect(error=ConnectionError())
|
||||
self._disconnect(error=ConnectionError())
|
||||
|
||||
def _start_reconnect(self):
|
||||
"""Starts a reconnection in the background."""
|
||||
|
@ -342,7 +342,7 @@ class MTProtoSender:
|
|||
|
||||
# Loops
|
||||
|
||||
async def _send_loop(self):
|
||||
def _send_loop(self):
|
||||
"""
|
||||
This loop is responsible for popping items off the send
|
||||
queue, encrypting them, and sending them over the network.
|
||||
|
@ -357,7 +357,7 @@ class MTProtoSender:
|
|||
self._send_queue.put_nowait(self._last_ack)
|
||||
self._pending_ack.clear()
|
||||
|
||||
messages = await self._send_queue.get()
|
||||
messages = self._send_queue.get()
|
||||
if messages == _reconnect_sentinel:
|
||||
if self._reconnecting:
|
||||
break
|
||||
|
@ -381,7 +381,7 @@ class MTProtoSender:
|
|||
while not any(m.future.cancelled() for m in messages):
|
||||
try:
|
||||
__log__.debug('Sending {} bytes...'.format(len(body)))
|
||||
await self._connection.send(body)
|
||||
self._connection.send(body)
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
|
@ -394,7 +394,7 @@ class MTProtoSender:
|
|||
__log__.warning('OSError while sending %s', e)
|
||||
else:
|
||||
__log__.exception('Unhandled exception while receiving')
|
||||
await asyncio.sleep(1)
|
||||
asyncio.sleep(1)
|
||||
|
||||
self._start_reconnect()
|
||||
break
|
||||
|
@ -411,7 +411,7 @@ class MTProtoSender:
|
|||
__log__.debug('Outgoing messages {} sent!'
|
||||
.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
|
||||
from the network, decrypting and handling or dispatching them.
|
||||
|
@ -421,7 +421,7 @@ class MTProtoSender:
|
|||
while self._user_connected and not self._reconnecting:
|
||||
try:
|
||||
__log__.debug('Receiving items from the network...')
|
||||
body = await self._connection.recv()
|
||||
body = self._connection.recv()
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
except asyncio.CancelledError:
|
||||
|
@ -433,7 +433,7 @@ class MTProtoSender:
|
|||
__log__.warning('OSError while receiving %s', e)
|
||||
else:
|
||||
__log__.exception('Unhandled exception while receiving')
|
||||
await asyncio.sleep(1)
|
||||
asyncio.sleep(1)
|
||||
|
||||
self._start_reconnect()
|
||||
break
|
||||
|
@ -469,20 +469,20 @@ class MTProtoSender:
|
|||
continue
|
||||
except:
|
||||
__log__.exception('Unhandled exception while unpacking')
|
||||
await asyncio.sleep(1)
|
||||
asyncio.sleep(1)
|
||||
else:
|
||||
try:
|
||||
await self._process_message(message)
|
||||
self._process_message(message)
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except:
|
||||
__log__.exception('Unhandled exception while '
|
||||
'processing %s', message)
|
||||
await asyncio.sleep(1)
|
||||
asyncio.sleep(1)
|
||||
|
||||
# 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
|
||||
acknowledged and dispatches control to different ``_handle_*``
|
||||
|
@ -491,9 +491,9 @@ class MTProtoSender:
|
|||
self._pending_ack.add(message.msg_id)
|
||||
handler = self._handlers.get(message.obj.CONSTRUCTOR_ID,
|
||||
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:
|
||||
|
||||
|
@ -529,7 +529,7 @@ class MTProtoSender:
|
|||
__log__.info('Received response without parent request: {}'
|
||||
.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:
|
||||
|
||||
|
@ -537,9 +537,9 @@ class MTProtoSender:
|
|||
"""
|
||||
__log__.debug('Handling container')
|
||||
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:
|
||||
|
||||
|
@ -548,15 +548,15 @@ class MTProtoSender:
|
|||
__log__.debug('Handling gzipped data')
|
||||
with BinaryReader(message.obj.data) as reader:
|
||||
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 {}'
|
||||
.format(message.obj.__class__.__name__))
|
||||
if self._update_callback:
|
||||
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``
|
||||
but are still sent through a request:
|
||||
|
@ -569,7 +569,7 @@ class MTProtoSender:
|
|||
if message:
|
||||
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
|
||||
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',
|
||||
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
|
||||
received bad message notification whenever possible:
|
||||
|
@ -640,7 +640,7 @@ class MTProtoSender:
|
|||
__log__.info('Message %d not resent due to bad msg',
|
||||
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:
|
||||
|
||||
|
@ -652,7 +652,7 @@ class MTProtoSender:
|
|||
__log__.debug('Handling detailed info for message %d', 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:
|
||||
|
||||
|
@ -664,7 +664,7 @@ class MTProtoSender:
|
|||
__log__.debug('Handling new detailed info for message %d', 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:
|
||||
|
||||
|
@ -675,7 +675,7 @@ class MTProtoSender:
|
|||
__log__.debug('Handling new session created')
|
||||
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
|
||||
these can be ignored except in the case of ``auth.logOut``:
|
||||
|
@ -701,7 +701,7 @@ class MTProtoSender:
|
|||
del self._pending_messages[msg_id]
|
||||
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
|
||||
``rpc_result`` but are still sent through a request:
|
||||
|
@ -716,7 +716,7 @@ class MTProtoSender:
|
|||
if msg:
|
||||
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
|
||||
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,
|
||||
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).
|
||||
"""
|
||||
|
@ -741,8 +741,8 @@ class _ContainerQueue(asyncio.Queue):
|
|||
``asyncio.Queue`` when needed for testing purposes, and
|
||||
a list won't be returned in said case.
|
||||
"""
|
||||
async def get(self):
|
||||
result = await super().get()
|
||||
def get(self):
|
||||
result = super().get()
|
||||
if self.empty() or result == _reconnect_sentinel or\
|
||||
isinstance(result.obj, MessageContainer):
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
pass
|
||||
|
|
|
@ -88,12 +88,12 @@ class Dialog:
|
|||
)
|
||||
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
|
||||
``client.send_message(dialog.input_entity, *args, **kwargs)``.
|
||||
"""
|
||||
return await self._client.send_message(
|
||||
return self._client.send_message(
|
||||
self.input_entity, *args, **kwargs)
|
||||
|
||||
def to_dict(self):
|
||||
|
|
|
@ -71,11 +71,11 @@ class Draft:
|
|||
|
||||
return self._input_entity
|
||||
|
||||
async def get_entity(self):
|
||||
def get_entity(self):
|
||||
"""
|
||||
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:
|
||||
self._entity =\
|
||||
self._client.get_entity(self._input_entity)
|
||||
|
@ -84,7 +84,7 @@ class Draft:
|
|||
|
||||
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.
|
||||
"""
|
||||
|
@ -115,7 +115,7 @@ class Draft:
|
|||
"""
|
||||
return not self._text
|
||||
|
||||
async def set_message(
|
||||
def set_message(
|
||||
self, text=None, reply_to=0, parse_mode=Default,
|
||||
link_preview=None):
|
||||
"""
|
||||
|
@ -144,9 +144,9 @@ class Draft:
|
|||
link_preview = self.link_preview
|
||||
|
||||
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,
|
||||
message=raw_text,
|
||||
no_webpage=not link_preview,
|
||||
|
@ -163,22 +163,22 @@ class Draft:
|
|||
|
||||
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
|
||||
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,
|
||||
link_preview=self.link_preview, parse_mode=parse_mode,
|
||||
clear_draft=clear
|
||||
)
|
||||
|
||||
async def delete(self):
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this draft, and returns ``True`` on success.
|
||||
"""
|
||||
return await self.set_message(text='')
|
||||
return self.set_message(text='')
|
||||
|
||||
def to_dict(self):
|
||||
try:
|
||||
|
|
|
@ -43,14 +43,14 @@ class Forward:
|
|||
"""
|
||||
return self._sender
|
||||
|
||||
async def get_sender(self):
|
||||
def get_sender(self):
|
||||
"""
|
||||
Returns `sender` but will make an API if necessary.
|
||||
"""
|
||||
if not self.sender and self.original_fwd.from_id:
|
||||
try:
|
||||
self._sender = await self._client.get_entity(
|
||||
await self.get_input_sender())
|
||||
self._sender = self._client.get_entity(
|
||||
self.get_input_sender())
|
||||
except ValueError:
|
||||
# TODO We could reload the message
|
||||
pass
|
||||
|
@ -71,7 +71,7 @@ class Forward:
|
|||
|
||||
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.
|
||||
"""
|
||||
|
@ -87,14 +87,14 @@ class Forward:
|
|||
"""
|
||||
return self._chat
|
||||
|
||||
async def get_chat(self):
|
||||
def get_chat(self):
|
||||
"""
|
||||
Returns `chat` but will make an API if necessary.
|
||||
"""
|
||||
if not self.chat and self.original_fwd.channel_id:
|
||||
try:
|
||||
self._chat = await self._client.get_entity(
|
||||
await self.get_input_chat())
|
||||
self._chat = self._client.get_entity(
|
||||
self.get_input_chat())
|
||||
except ValueError:
|
||||
# TODO We could reload the message
|
||||
pass
|
||||
|
@ -115,7 +115,7 @@ class Forward:
|
|||
|
||||
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.
|
||||
"""
|
||||
|
|
|
@ -147,14 +147,14 @@ class Message:
|
|||
return self.original_message.action
|
||||
|
||||
# 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,
|
||||
along with their input versions.
|
||||
"""
|
||||
try:
|
||||
chat = await self.get_input_chat() if self.is_channel else None
|
||||
msg = await self._client.get_messages(
|
||||
chat = self.get_input_chat() if self.is_channel else None
|
||||
msg = self._client.get_messages(
|
||||
chat, ids=self.original_message.id)
|
||||
except ValueError:
|
||||
return # We may not have the input chat/get message failed
|
||||
|
@ -177,17 +177,17 @@ class Message:
|
|||
"""
|
||||
return self._sender
|
||||
|
||||
async def get_sender(self):
|
||||
def get_sender(self):
|
||||
"""
|
||||
Returns `sender`, but will make an API call to find the
|
||||
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:
|
||||
self._sender =\
|
||||
await self._client.get_entity(self._input_sender)
|
||||
self._client.get_entity(self._input_sender)
|
||||
except ValueError:
|
||||
await self._reload_message()
|
||||
self._reload_message()
|
||||
return self._sender
|
||||
|
||||
@property
|
||||
|
@ -201,17 +201,17 @@ class Message:
|
|||
"""
|
||||
return self._chat
|
||||
|
||||
async def get_chat(self):
|
||||
def get_chat(self):
|
||||
"""
|
||||
Returns `chat`, but will make an API call to find the
|
||||
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:
|
||||
self._chat =\
|
||||
await self._client.get_entity(self._input_chat)
|
||||
self._client.get_entity(self._input_chat)
|
||||
except ValueError:
|
||||
await self._reload_message()
|
||||
self._reload_message()
|
||||
return self._chat
|
||||
|
||||
@property
|
||||
|
@ -234,14 +234,14 @@ class Message:
|
|||
pass
|
||||
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
|
||||
input sender unless it's already cached.
|
||||
"""
|
||||
if self.input_sender is None\
|
||||
and not self.is_channel and not self.is_group:
|
||||
await self._reload_message()
|
||||
self._reload_message()
|
||||
return self._input_sender
|
||||
|
||||
@property
|
||||
|
@ -263,7 +263,7 @@ class Message:
|
|||
|
||||
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
|
||||
input chat unless it's already cached.
|
||||
|
@ -273,7 +273,7 @@ class Message:
|
|||
# The input chat cannot rely on ._reload_message() because
|
||||
# said method may need the input chat.
|
||||
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:
|
||||
self._chat = d.entity
|
||||
self._input_chat = d.input_entity
|
||||
|
@ -385,20 +385,20 @@ class Message:
|
|||
|
||||
return self._buttons
|
||||
|
||||
async def get_buttons(self):
|
||||
def get_buttons(self):
|
||||
"""
|
||||
Returns `buttons`, but will make an API call to find the
|
||||
input chat (needed for the buttons) unless it's already cached.
|
||||
"""
|
||||
if not self.buttons and isinstance(
|
||||
self.original_message, types.Message):
|
||||
chat = await self.get_input_chat()
|
||||
chat = self.get_input_chat()
|
||||
if not chat:
|
||||
return
|
||||
try:
|
||||
bot = self._needed_markup_bot()
|
||||
except ValueError:
|
||||
await self._reload_message()
|
||||
self._reload_message()
|
||||
bot = self._needed_markup_bot() # TODO use via_input_bot
|
||||
|
||||
self._set_buttons(chat, bot)
|
||||
|
@ -521,7 +521,7 @@ class Message:
|
|||
"""
|
||||
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
|
||||
to, or ``None``.
|
||||
|
@ -532,33 +532,33 @@ class Message:
|
|||
if self._reply_message is None:
|
||||
if not self.original_message.reply_to_msg_id:
|
||||
return None
|
||||
self._reply_message = await self._client.get_messages(
|
||||
await self.get_input_chat() if self.is_channel else None,
|
||||
self._reply_message = self._client.get_messages(
|
||||
self.get_input_chat() if self.is_channel else None,
|
||||
ids=self.original_message.reply_to_msg_id
|
||||
)
|
||||
|
||||
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
|
||||
`telethon.telegram_client.TelegramClient.send_message` with
|
||||
``entity`` already set.
|
||||
"""
|
||||
return await self._client.send_message(
|
||||
await self.get_input_chat(), *args, **kwargs)
|
||||
return self._client.send_message(
|
||||
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
|
||||
`telethon.telegram_client.TelegramClient.send_message` with
|
||||
both ``entity`` and ``reply_to`` already set.
|
||||
"""
|
||||
kwargs['reply_to'] = self.original_message.id
|
||||
return await self._client.send_message(
|
||||
await self.get_input_chat(), *args, **kwargs)
|
||||
return self._client.send_message(
|
||||
self.get_input_chat(), *args, **kwargs)
|
||||
|
||||
async def forward_to(self, *args, **kwargs):
|
||||
def forward_to(self, *args, **kwargs):
|
||||
"""
|
||||
Forwards the message. Shorthand for
|
||||
`telethon.telegram_client.TelegramClient.forward_messages` with
|
||||
|
@ -569,10 +569,10 @@ class Message:
|
|||
`telethon.telegram_client.TelegramClient` instance directly.
|
||||
"""
|
||||
kwargs['messages'] = self.original_message.id
|
||||
kwargs['from_peer'] = await self.get_input_chat()
|
||||
return await self._client.forward_messages(*args, **kwargs)
|
||||
kwargs['from_peer'] = self.get_input_chat()
|
||||
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
|
||||
`telethon.telegram_client.TelegramClient.edit_message` with
|
||||
|
@ -590,12 +590,12 @@ class Message:
|
|||
if self.original_message.to_id.user_id != me.user_id:
|
||||
return None
|
||||
|
||||
return await self._client.edit_message(
|
||||
await self.get_input_chat(), self.original_message,
|
||||
return self._client.edit_message(
|
||||
self.get_input_chat(), self.original_message,
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
async def delete(self, *args, **kwargs):
|
||||
def delete(self, *args, **kwargs):
|
||||
"""
|
||||
Deletes the message. You're responsible for checking whether you
|
||||
have the permission to do so, or to except the error otherwise.
|
||||
|
@ -607,18 +607,18 @@ class Message:
|
|||
this `delete` method. Use a
|
||||
`telethon.telegram_client.TelegramClient` instance directly.
|
||||
"""
|
||||
return await self._client.delete_messages(
|
||||
await self.get_input_chat(), [self.original_message],
|
||||
return self._client.delete_messages(
|
||||
self.get_input_chat(), [self.original_message],
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
async def download_media(self, *args, **kwargs):
|
||||
def download_media(self, *args, **kwargs):
|
||||
"""
|
||||
Downloads the media contained in the message, if any.
|
||||
`telethon.telegram_client.TelegramClient.download_media` with
|
||||
the ``message`` already set.
|
||||
"""
|
||||
return await self._client.download_media(
|
||||
return self._client.download_media(
|
||||
self.original_message, *args, **kwargs)
|
||||
|
||||
def get_entities_text(self, cls=None):
|
||||
|
@ -647,7 +647,7 @@ class Message:
|
|||
texts = get_inner_text(self.original_message.message, ent)
|
||||
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`
|
||||
for the specified button.
|
||||
|
@ -691,32 +691,32 @@ class Message:
|
|||
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')
|
||||
|
||||
if not await self.get_buttons():
|
||||
if not self.get_buttons():
|
||||
return # Accessing the property sets self._buttons[_flat]
|
||||
|
||||
if text is not None:
|
||||
if callable(text):
|
||||
for button in self._buttons_flat:
|
||||
if text(button.text):
|
||||
return await button.click()
|
||||
return button.click()
|
||||
else:
|
||||
for button in self._buttons_flat:
|
||||
if button.text == text:
|
||||
return await button.click()
|
||||
return button.click()
|
||||
return
|
||||
|
||||
if filter is not None:
|
||||
for button in self._buttons_flat:
|
||||
if filter(button):
|
||||
return await button.click()
|
||||
return button.click()
|
||||
return
|
||||
|
||||
if i is None:
|
||||
i = 0
|
||||
if j is None:
|
||||
return await self._buttons_flat[i].click()
|
||||
return self._buttons_flat[i].click()
|
||||
else:
|
||||
return await self._buttons[i][j].click()
|
||||
return self._buttons[i][j].click()
|
||||
|
||||
|
||||
class _CustomMessage(Message, types.Message):
|
||||
|
|
|
@ -52,7 +52,7 @@ class MessageButton:
|
|||
if isinstance(self.button, types.KeyboardButtonUrl):
|
||||
return self.button.url
|
||||
|
||||
async def click(self):
|
||||
def click(self):
|
||||
"""
|
||||
Emulates the behaviour of clicking this button.
|
||||
|
||||
|
@ -70,18 +70,18 @@ class MessageButton:
|
|||
be passed to ``webbrowser.open`` and return ``True`` on success.
|
||||
"""
|
||||
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)
|
||||
elif isinstance(self.button, types.KeyboardButtonCallback):
|
||||
req = functions.messages.GetBotCallbackAnswerRequest(
|
||||
peer=self._chat, msg_id=self._msg_id, data=self.button.data
|
||||
)
|
||||
try:
|
||||
return await self._client(req)
|
||||
return self._client(req)
|
||||
except BotTimeout:
|
||||
return None
|
||||
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
|
||||
))
|
||||
elif isinstance(self.button, types.KeyboardButtonUrl):
|
||||
|
|
|
@ -162,5 +162,5 @@ class TLRequest(TLObject):
|
|||
def read_result(reader):
|
||||
return reader.tgread_object()
|
||||
|
||||
async def resolve(self, client, utils):
|
||||
def resolve(self, client, utils):
|
||||
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 = {
|
||||
'InputPeer':
|
||||
'utils.get_input_peer(await client.get_input_entity({}))',
|
||||
'utils.get_input_peer(client.get_input_entity({}))',
|
||||
'InputChannel':
|
||||
'utils.get_input_channel(await client.get_input_entity({}))',
|
||||
'utils.get_input_channel(client.get_input_entity({}))',
|
||||
'InputUser':
|
||||
'utils.get_input_user(await client.get_input_entity({}))',
|
||||
'utils.get_input_user(client.get_input_entity({}))',
|
||||
'InputDialogPeer':
|
||||
'utils.get_input_dialog(await client.get_input_entity({}))',
|
||||
'utils.get_input_dialog(client.get_input_entity({}))',
|
||||
|
||||
'InputMedia': 'utils.get_input_media({})',
|
||||
'InputPhoto': 'utils.get_input_photo({})',
|
||||
|
@ -233,7 +233,7 @@ def _write_class_init(tlobject, kind, type_constructors, builder):
|
|||
|
||||
def _write_resolve(tlobject, builder):
|
||||
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:
|
||||
ac = AUTO_CASTS.get(arg.type, None)
|
||||
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