Remove all async/await

This commit is contained in:
Lonami Exo 2018-06-28 09:05:00 +02:00
parent 3154575ab6
commit 62c6565189
91 changed files with 438 additions and 7493 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
telethon
========
.. toctree::
:maxdepth: 3
telethon

View File

@ -1 +0,0 @@
telethon

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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