mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-01-24 08:14:14 +03:00
Reboot project
This commit is contained in:
parent
c3bddf9440
commit
fb41cc0546
|
@ -1,8 +0,0 @@
|
|||
[run]
|
||||
branch = true
|
||||
parallel = true
|
||||
source =
|
||||
telethon
|
||||
|
||||
[report]
|
||||
precision = 2
|
25
.gitignore
vendored
25
.gitignore
vendored
|
@ -1,23 +1,4 @@
|
|||
# Generated code
|
||||
/telethon/tl/functions/
|
||||
/telethon/tl/types/
|
||||
/telethon/tl/alltlobjects.py
|
||||
/telethon/errors/rpcerrorlist.py
|
||||
|
||||
# User session
|
||||
*.session
|
||||
/usermedia/
|
||||
|
||||
# Builds and testing
|
||||
__pycache__/
|
||||
/dist/
|
||||
/build/
|
||||
/*.egg-info/
|
||||
/readthedocs/_build/
|
||||
/.tox/
|
||||
|
||||
# API reference docs
|
||||
/docs/
|
||||
|
||||
# File used to manually test new changes, contains sensitive data
|
||||
/example.py
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.pytest_cache/
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
||||
sha: 7539d8bd1a00a3c1bfd34cdb606d3a6372e83469
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
- id: check-symlinks
|
||||
- id: check-yaml
|
||||
- id: double-quote-string-fixer
|
||||
- id: end-of-file-fixer
|
||||
- id: name-tests-test
|
||||
- id: trailing-whitespace
|
||||
- repo: git://github.com/pre-commit/mirrors-yapf
|
||||
sha: v0.11.1
|
||||
hooks:
|
||||
- id: yapf
|
||||
- repo: git://github.com/FalconSocial/pre-commit-python-sorter
|
||||
sha: 1.0.4
|
||||
hooks:
|
||||
- id: python-import-sorter
|
||||
args:
|
||||
- --silent-overwrite
|
32
README.rst
32
README.rst
|
@ -1,5 +1,6 @@
|
|||
Telethon
|
||||
========
|
||||
|
||||
.. epigraph::
|
||||
|
||||
⭐️ Thanks **everyone** who has starred the project, it means a lot!
|
||||
|
@ -10,11 +11,12 @@ as a user or through a bot account (bot API alternative).
|
|||
|
||||
.. important::
|
||||
|
||||
If you have code using Telethon before its 1.0 version, you must
|
||||
read `Compatibility and Convenience`_ to learn how to migrate.
|
||||
If you have code using Telethon before its 2.0 version, it is strongly
|
||||
recommended to read the Migration Guide section in the documentation.
|
||||
As with any third-party library for Telegram, be careful not to
|
||||
break `Telegram's ToS`_ or `Telegram can ban the account`_.
|
||||
|
||||
|
||||
What is this?
|
||||
-------------
|
||||
|
||||
|
@ -29,7 +31,7 @@ Installing
|
|||
|
||||
.. code-block:: sh
|
||||
|
||||
pip3 install telethon
|
||||
pip install telethon
|
||||
|
||||
|
||||
Creating a client
|
||||
|
@ -44,8 +46,8 @@ Creating a client
|
|||
api_id = 12345
|
||||
api_hash = '0123456789abcdef0123456789abcdef'
|
||||
|
||||
client = TelegramClient('session_name', api_id, api_hash)
|
||||
client.start()
|
||||
async with TelegramClient('session_name', api_id, api_hash) as client:
|
||||
...
|
||||
|
||||
|
||||
Doing stuff
|
||||
|
@ -53,34 +55,28 @@ Doing stuff
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
print(client.get_me().stringify())
|
||||
print(await client.get_me())
|
||||
|
||||
client.send_message('username', 'Hello! Talking to you from Telethon')
|
||||
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
|
||||
await client.send_message('username', 'Hello! Talking to you from Telethon')
|
||||
await client.send_message('username', photo='/home/myself/Pictures/holidays.jpg')
|
||||
|
||||
client.download_profile_photo('me')
|
||||
messages = client.get_messages('username')
|
||||
messages[0].download_media()
|
||||
|
||||
@client.on(events.NewMessage(pattern='(?i)hi|hello'))
|
||||
async def handler(event):
|
||||
await event.respond('Hey!')
|
||||
async for message in client.get_messages('username', 1):
|
||||
path = await message.download_media()
|
||||
print('Saved media to', path)
|
||||
|
||||
|
||||
Next steps
|
||||
----------
|
||||
|
||||
Do you like how Telethon looks? Check out `Read The Docs`_ for a more
|
||||
Do you like how Telethon looks? Check out the documentation for a more
|
||||
in-depth explanation, with examples, troubleshooting issues, and more
|
||||
useful information.
|
||||
|
||||
.. _asyncio: https://docs.python.org/3/library/asyncio.html
|
||||
.. _MTProto: https://core.telegram.org/mtproto
|
||||
.. _Telegram: https://telegram.org
|
||||
.. _Compatibility and Convenience: https://docs.telethon.dev/en/stable/misc/compatibility-and-convenience.html
|
||||
.. _Telegram's ToS: https://core.telegram.org/api/terms
|
||||
.. _Telegram can ban the account: https://docs.telethon.dev/en/stable/quick-references/faq.html#my-account-was-deleted-limited-when-using-the-library
|
||||
.. _Read The Docs: https://docs.telethon.dev
|
||||
|
||||
.. |logo| image:: logo.svg
|
||||
:width: 24pt
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
pytest
|
||||
pytest-cov
|
||||
pytest-asyncio
|
|
@ -1,5 +0,0 @@
|
|||
cryptg
|
||||
pysocks
|
||||
python-socks[asyncio]
|
||||
hachoir
|
||||
pillow
|
|
@ -1,36 +0,0 @@
|
|||
# https://snarky.ca/what-the-heck-is-pyproject-toml/
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
# Need to use legacy format for the time being
|
||||
# https://tox.readthedocs.io/en/3.20.0/example/basic.html#pyproject-toml-tox-legacy-ini
|
||||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
[tox]
|
||||
envlist = py35,py36,py37,py38
|
||||
|
||||
# run with tox -e py
|
||||
[testenv]
|
||||
deps =
|
||||
-rrequirements.txt
|
||||
-roptional-requirements.txt
|
||||
-rdev-requirements.txt
|
||||
commands =
|
||||
# NOTE: you can run any command line tool here - not just tests
|
||||
pytest {posargs}
|
||||
|
||||
# run with tox -e flake
|
||||
[testenv:flake]
|
||||
deps =
|
||||
-rrequirements.txt
|
||||
-roptional-requirements.txt
|
||||
-rdev-requirements.txt
|
||||
flake8
|
||||
commands =
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 telethon/ telethon_generator/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 telethon/ telethon_generator/ tests/ --count --exit-zero --exclude telethon/tl/,telethon/errors/rpcerrorlist.py --max-complexity=10 --max-line-length=127 --statistics
|
||||
|
||||
"""
|
|
@ -1,20 +0,0 @@
|
|||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = Telethon
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
@ -1,96 +0,0 @@
|
|||
.. _installation:
|
||||
|
||||
============
|
||||
Installation
|
||||
============
|
||||
|
||||
Telethon is a Python library, which means you need to download and install
|
||||
Python from https://www.python.org/downloads/ if you haven't already. Once
|
||||
you have Python installed, `upgrade pip`__ and run:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install --upgrade telethon
|
||||
|
||||
…to install or upgrade the library to the latest version.
|
||||
|
||||
.. __: https://pythonspeed.com/articles/upgrade-pip/
|
||||
|
||||
Installing Development Versions
|
||||
===============================
|
||||
|
||||
If you want the *latest* unreleased changes,
|
||||
you can run the following command instead:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
python3 -m pip install --upgrade https://github.com/LonamiWebs/Telethon/archive/v1.zip
|
||||
|
||||
.. note::
|
||||
|
||||
The development version may have bugs and is not recommended for production
|
||||
use. However, when you are `reporting a library bug`__, you should try if the
|
||||
bug still occurs in this version.
|
||||
|
||||
.. __: https://github.com/LonamiWebs/Telethon/issues/
|
||||
|
||||
|
||||
Verification
|
||||
============
|
||||
|
||||
To verify that the library is installed correctly, run the following command:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
python3 -c "import telethon; print(telethon.__version__)"
|
||||
|
||||
The version number of the library should show in the output.
|
||||
|
||||
|
||||
Optional Dependencies
|
||||
=====================
|
||||
|
||||
If cryptg_ is installed, **the library will work a lot faster**, since
|
||||
encryption and decryption will be made in C instead of Python. If your
|
||||
code deals with a lot of updates or you are downloading/uploading a lot
|
||||
of files, you will notice a considerable speed-up (from a hundred kilobytes
|
||||
per second to several megabytes per second, if your connection allows it).
|
||||
If it's not installed, pyaes_ will be used (which is pure Python, so it's
|
||||
much slower).
|
||||
|
||||
If pillow_ is installed, large images will be automatically resized when
|
||||
sending photos to prevent Telegram from failing with "invalid image".
|
||||
Official clients also do this.
|
||||
|
||||
If aiohttp_ is installed, the library will be able to download
|
||||
:tl:`WebDocument` media files (otherwise you will get an error).
|
||||
|
||||
If hachoir_ is installed, it will be used to extract metadata from files
|
||||
when sending documents. Telegram uses this information to show the song's
|
||||
performer, artist, title, duration, and for videos too (including size).
|
||||
Otherwise, they will default to empty values, and you can set the attributes
|
||||
manually.
|
||||
|
||||
.. note::
|
||||
|
||||
Some of the modules may require additional dependencies before being
|
||||
installed through ``pip``. If you have an ``apt``-based system, consider
|
||||
installing the most commonly missing dependencies (with the right ``pip``):
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
apt update
|
||||
apt install clang lib{jpeg-turbo,webp}-dev python{,-dev} zlib-dev
|
||||
pip install -U --user setuptools
|
||||
pip install -U --user telethon cryptg pillow
|
||||
|
||||
Thanks to `@bb010g`_ for writing down this nice list.
|
||||
|
||||
|
||||
.. _cryptg: https://github.com/cher-nov/cryptg
|
||||
.. _pyaes: https://github.com/ricmoo/pyaes
|
||||
.. _pillow: https://python-pillow.org
|
||||
.. _aiohttp: https://docs.aiohttp.org
|
||||
.. _hachoir: https://hachoir.readthedocs.io
|
||||
.. _@bb010g: https://static.bb010g.com
|
|
@ -1,46 +0,0 @@
|
|||
==========
|
||||
Next Steps
|
||||
==========
|
||||
|
||||
These basic first steps should have gotten you started with the library.
|
||||
|
||||
By now, you should know how to call friendly methods and how to work with
|
||||
the returned objects, how things work inside event handlers, etc.
|
||||
|
||||
Next, we will see a quick reference summary of *all* the methods and
|
||||
properties that you will need when using the library. If you follow
|
||||
the links there, you will expand the documentation for the method
|
||||
and property, with more examples on how to use them.
|
||||
|
||||
Therefore, **you can find an example on every method** of the client
|
||||
to learn how to use it, as well as a description of all the arguments.
|
||||
|
||||
After that, we will go in-depth with some other important concepts
|
||||
that are worth learning and understanding.
|
||||
|
||||
From now on, you can keep pressing the "Next" button if you want,
|
||||
or use the menu on the left, since some pages are quite lengthy.
|
||||
|
||||
A note on developing applications
|
||||
=================================
|
||||
|
||||
If you're using the library to make an actual application (and not just
|
||||
automate things), you should make sure to `comply with the ToS`__:
|
||||
|
||||
[…] when logging in as an existing user, apps are supposed to call
|
||||
[:tl:`GetTermsOfServiceUpdate`] to check for any updates to the Terms of
|
||||
Service; this call should be repeated after ``expires`` seconds have
|
||||
elapsed. If an update to the Terms Of Service is available, clients are
|
||||
supposed to show a consent popup; if accepted, clients should call
|
||||
[:tl:`AcceptTermsOfService`], providing the ``termsOfService id`` JSON
|
||||
object; in case of denial, clients are to delete the account using
|
||||
[:tl:`DeleteAccount`], providing Decline ToS update as deletion reason.
|
||||
|
||||
.. __: https://core.telegram.org/api/config#terms-of-service
|
||||
|
||||
However, if you use the library to automate or enhance your Telegram
|
||||
experience, it's very likely that you are using other applications doing this
|
||||
check for you (so you wouldn't run the risk of violating the ToS).
|
||||
|
||||
The library itself will not automatically perform this check or accept the ToS
|
||||
because it should require user action (the only exception is during sign-up).
|
|
@ -1,111 +0,0 @@
|
|||
===========
|
||||
Quick-Start
|
||||
===========
|
||||
|
||||
Let's see a longer example to learn some of the methods that the library
|
||||
has to offer. These are known as "friendly methods", and you should always
|
||||
use these if possible.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient
|
||||
|
||||
# Remember to use your own values from my.telegram.org!
|
||||
api_id = 12345
|
||||
api_hash = '0123456789abcdef0123456789abcdef'
|
||||
client = TelegramClient('anon', api_id, api_hash)
|
||||
|
||||
async def main():
|
||||
# Getting information about yourself
|
||||
me = await client.get_me()
|
||||
|
||||
# "me" is a user object. You can pretty-print
|
||||
# any Telegram object with the "stringify" method:
|
||||
print(me.stringify())
|
||||
|
||||
# When you print something, you see a representation of it.
|
||||
# You can access all attributes of Telegram objects with
|
||||
# the dot operator. For example, to get the username:
|
||||
username = me.username
|
||||
print(username)
|
||||
print(me.phone)
|
||||
|
||||
# You can print all the dialogs/conversations that you are part of:
|
||||
async for dialog in client.iter_dialogs():
|
||||
print(dialog.name, 'has ID', dialog.id)
|
||||
|
||||
# You can send messages to yourself...
|
||||
await client.send_message('me', 'Hello, myself!')
|
||||
# ...to some chat ID
|
||||
await client.send_message(-100123456, 'Hello, group!')
|
||||
# ...to your contacts
|
||||
await client.send_message('+34600123123', 'Hello, friend!')
|
||||
# ...or even to any username
|
||||
await client.send_message('username', 'Testing Telethon!')
|
||||
|
||||
# You can, of course, use markdown in your messages:
|
||||
message = await client.send_message(
|
||||
'me',
|
||||
'This message has **bold**, `code`, __italics__ and '
|
||||
'a [nice website](https://example.com)!',
|
||||
link_preview=False
|
||||
)
|
||||
|
||||
# Sending a message returns the sent message object, which you can use
|
||||
print(message.raw_text)
|
||||
|
||||
# You can reply to messages directly if you have a message object
|
||||
await message.reply('Cool!')
|
||||
|
||||
# Or send files, songs, documents, albums...
|
||||
await client.send_file('me', '/home/me/Pictures/holidays.jpg')
|
||||
|
||||
# You can print the message history of any chat:
|
||||
async for message in client.iter_messages('me'):
|
||||
print(message.id, message.text)
|
||||
|
||||
# You can download media from messages, too!
|
||||
# The method will return the path where the file was saved.
|
||||
if message.photo:
|
||||
path = await message.download_media()
|
||||
print('File saved to', path) # printed after download is done
|
||||
|
||||
with client:
|
||||
client.loop.run_until_complete(main())
|
||||
|
||||
|
||||
Here, we show how to sign in, get information about yourself, send
|
||||
messages, files, getting chats, printing messages, and downloading
|
||||
files.
|
||||
|
||||
You should make sure that you understand what the code shown here
|
||||
does, take note on how methods are called and used and so on before
|
||||
proceeding. We will see all the available methods later on.
|
||||
|
||||
.. important::
|
||||
|
||||
Note that Telethon is an asynchronous library, and as such, you should
|
||||
get used to it and learn a bit of basic `asyncio`. This will help a lot.
|
||||
As a quick start, this means you generally want to write all your code
|
||||
inside some ``async def`` like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client = ...
|
||||
|
||||
async def do_something(me):
|
||||
...
|
||||
|
||||
async def main():
|
||||
# Most of your code should go here.
|
||||
# You can of course make and use your own async def (do_something).
|
||||
# They only need to be async if they need to await things.
|
||||
me = await client.get_me()
|
||||
await do_something(me)
|
||||
|
||||
with client:
|
||||
client.loop.run_until_complete(main())
|
||||
|
||||
After you understand this, you may use the ``telethon.sync`` hack if you
|
||||
want do so (see :ref:`compatibility-and-convenience`), but note you may
|
||||
run into other issues (iPython, Anaconda, etc. have some issues with it).
|
|
@ -1,229 +0,0 @@
|
|||
.. _signing-in:
|
||||
|
||||
==========
|
||||
Signing In
|
||||
==========
|
||||
|
||||
Before working with Telegram's API, you need to get your own API ID and hash:
|
||||
|
||||
1. `Login to your Telegram account <https://my.telegram.org/>`_ with the
|
||||
phone number of the developer account to use.
|
||||
|
||||
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!
|
||||
|
||||
.. note::
|
||||
|
||||
This API ID and hash is the one used by *your application*, not your
|
||||
phone number. You can use this API ID and hash with *any* phone number
|
||||
or even for bot accounts.
|
||||
|
||||
|
||||
Editing the Code
|
||||
================
|
||||
|
||||
This is a little introduction for those new to Python programming in general.
|
||||
|
||||
We will write our code inside ``hello.py``, so you can use any text
|
||||
editor that you like. To run the code, use ``python3 hello.py`` from
|
||||
the terminal.
|
||||
|
||||
.. important::
|
||||
|
||||
Don't call your script ``telethon.py``! Python will try to import
|
||||
the client from there and it will fail with an error such as
|
||||
"ImportError: cannot import name 'TelegramClient' ...".
|
||||
|
||||
|
||||
Signing In
|
||||
==========
|
||||
|
||||
We can finally write some code to log into our account!
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient
|
||||
|
||||
# Use your own values from my.telegram.org
|
||||
api_id = 12345
|
||||
api_hash = '0123456789abcdef0123456789abcdef'
|
||||
|
||||
# The first parameter is the .session file name (absolute paths allowed)
|
||||
with TelegramClient('anon', api_id, api_hash) as client:
|
||||
client.loop.run_until_complete(client.send_message('me', 'Hello, myself!'))
|
||||
|
||||
|
||||
In the first line, we import the class name so we can create an instance
|
||||
of the client. Then, we define variables to store our API ID and hash
|
||||
conveniently.
|
||||
|
||||
At last, we create a new `TelegramClient <telethon.client.telegramclient.TelegramClient>`
|
||||
instance and call it ``client``. We can now use the client variable
|
||||
for anything that we want, such as sending a message to ourselves.
|
||||
|
||||
.. note::
|
||||
|
||||
Since Telethon is an asynchronous library, you need to ``await``
|
||||
coroutine functions to have them run (or otherwise, run the loop
|
||||
until they are complete). In this tiny example, we don't bother
|
||||
making an ``async def main()``.
|
||||
|
||||
See :ref:`mastering-asyncio` to find out more.
|
||||
|
||||
|
||||
Using a ``with`` block is the preferred way to use the library. It will
|
||||
automatically `start() <telethon.client.auth.AuthMethods.start>` the client,
|
||||
logging or signing up if necessary.
|
||||
|
||||
If the ``.session`` file already existed, it will not login
|
||||
again, so be aware of this if you move or rename the file!
|
||||
|
||||
|
||||
Signing In as a Bot Account
|
||||
===========================
|
||||
|
||||
You can also use Telethon for your bots (normal bot accounts, not users).
|
||||
You will still need an API ID and hash, but the process is very similar:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.sync import TelegramClient
|
||||
|
||||
api_id = 12345
|
||||
api_hash = '0123456789abcdef0123456789abcdef'
|
||||
bot_token = '12345:0123456789abcdef0123456789abcdef'
|
||||
|
||||
# We have to manually call "start" if we want an explicit bot token
|
||||
bot = TelegramClient('bot', api_id, api_hash).start(bot_token=bot_token)
|
||||
|
||||
# But then we can use the client instance as usual
|
||||
with bot:
|
||||
...
|
||||
|
||||
|
||||
To get a bot account, you need to talk
|
||||
with `@BotFather <https://t.me/BotFather>`_.
|
||||
|
||||
|
||||
Signing In behind a Proxy
|
||||
=========================
|
||||
|
||||
If you need to use a proxy to access Telegram,
|
||||
you will need to either:
|
||||
|
||||
* For Python >= 3.6 : `install python-socks[asyncio]`__
|
||||
* For Python <= 3.5 : `install PySocks`__
|
||||
|
||||
and then change
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
TelegramClient('anon', api_id, api_hash)
|
||||
|
||||
with
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
TelegramClient('anon', api_id, api_hash, proxy=("socks5", '127.0.0.1', 4444))
|
||||
|
||||
(of course, replacing the protocol, IP and port with the protocol, IP and port of the proxy).
|
||||
|
||||
The ``proxy=`` argument should be a dict (or tuple, for backwards compatibility),
|
||||
consisting of parameters described `in PySocks usage`__.
|
||||
|
||||
The allowed values for the argument ``proxy_type`` are:
|
||||
|
||||
* For Python <= 3.5:
|
||||
* ``socks.SOCKS5`` or ``'socks5'``
|
||||
* ``socks.SOCKS4`` or ``'socks4'``
|
||||
* ``socks.HTTP`` or ``'http'``
|
||||
|
||||
* For Python >= 3.6:
|
||||
* All of the above
|
||||
* ``python_socks.ProxyType.SOCKS5``
|
||||
* ``python_socks.ProxyType.SOCKS4``
|
||||
* ``python_socks.ProxyType.HTTP``
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
proxy = {
|
||||
'proxy_type': 'socks5', # (mandatory) protocol to use (see above)
|
||||
'addr': '1.1.1.1', # (mandatory) proxy IP address
|
||||
'port': 5555, # (mandatory) proxy port number
|
||||
'username': 'foo', # (optional) username if the proxy requires auth
|
||||
'password': 'bar', # (optional) password if the proxy requires auth
|
||||
'rdns': True # (optional) whether to use remote or local resolve, default remote
|
||||
}
|
||||
|
||||
For backwards compatibility with ``PySocks`` the following format
|
||||
is possible (but discouraged):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
proxy = (socks.SOCKS5, '1.1.1.1', 5555, True, 'foo', 'bar')
|
||||
|
||||
.. __: https://github.com/romis2012/python-socks#installation
|
||||
.. __: https://github.com/Anorov/PySocks#installation
|
||||
.. __: https://github.com/Anorov/PySocks#usage-1
|
||||
|
||||
|
||||
Using MTProto Proxies
|
||||
=====================
|
||||
|
||||
MTProto Proxies are Telegram's alternative to normal proxies,
|
||||
and work a bit differently. The following protocols are available:
|
||||
|
||||
* ``ConnectionTcpMTProxyAbridged``
|
||||
* ``ConnectionTcpMTProxyIntermediate``
|
||||
* ``ConnectionTcpMTProxyRandomizedIntermediate`` (preferred)
|
||||
|
||||
For now, you need to manually specify these special connection modes
|
||||
if you want to use a MTProto Proxy. Your code would look like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, connection
|
||||
# we need to change the connection ^^^^^^^^^^
|
||||
|
||||
client = TelegramClient(
|
||||
'anon',
|
||||
api_id,
|
||||
api_hash,
|
||||
|
||||
# Use one of the available connection modes.
|
||||
# Normally, this one works with most proxies.
|
||||
connection=connection.ConnectionTcpMTProxyRandomizedIntermediate,
|
||||
|
||||
# Then, pass the proxy details as a tuple:
|
||||
# (host name, port, proxy secret)
|
||||
#
|
||||
# If the proxy has no secret, the secret must be:
|
||||
# '00000000000000000000000000000000'
|
||||
proxy=('mtproxy.example.com', 2002, 'secret')
|
||||
)
|
||||
|
||||
In future updates, we may make it easier to use MTProto Proxies
|
||||
(such as avoiding the need to manually pass ``connection=``).
|
||||
|
||||
In short, the same code above but without comments to make it clearer:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, connection
|
||||
|
||||
client = TelegramClient(
|
||||
'anon', api_id, api_hash,
|
||||
connection=connection.ConnectionTcpMTProxyRandomizedIntermediate,
|
||||
proxy=('mtproxy.example.com', 2002, 'secret')
|
||||
)
|
|
@ -1,159 +0,0 @@
|
|||
=======
|
||||
Updates
|
||||
=======
|
||||
|
||||
Updates are an important topic in a messaging platform like Telegram.
|
||||
After all, you want to be notified when a new message arrives, when
|
||||
a member joins, when someone starts typing, etc.
|
||||
For that, you can use **events**.
|
||||
|
||||
.. important::
|
||||
|
||||
It is strongly advised to enable logging when working with events,
|
||||
since exceptions in event handlers are hidden by default. Please
|
||||
add the following snippet to the very top of your file:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import logging
|
||||
logging.basicConfig(format='[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s',
|
||||
level=logging.WARNING)
|
||||
|
||||
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
Let's start things with an example to automate replies:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, events
|
||||
|
||||
client = TelegramClient('anon', 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()
|
||||
|
||||
|
||||
This code isn't much, but there might be some things unclear.
|
||||
Let's break it down:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, events
|
||||
|
||||
client = TelegramClient('anon', 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.
|
||||
|
||||
.. note::
|
||||
|
||||
Event handlers **must** be ``async def``. After all,
|
||||
Telethon is an asynchronous library based on `asyncio`,
|
||||
which is a safer and often faster approach to threads.
|
||||
|
||||
You **must** ``await`` all method calls that use
|
||||
network requests, which is most of them.
|
||||
|
||||
|
||||
More Examples
|
||||
=============
|
||||
|
||||
Replying to messages with hello is fun, but, can we do more?
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@client.on(events.NewMessage(outgoing=True, pattern=r'\.save'))
|
||||
async def handler(event):
|
||||
if event.is_reply:
|
||||
replied = await event.get_reply_message()
|
||||
sender = replied.sender
|
||||
await client.download_profile_photo(sender)
|
||||
await event.respond('Saved your photo {}'.format(sender.username))
|
||||
|
||||
We could also get replies. This event filters outgoing messages
|
||||
(only those that we send will trigger the method), then we filter
|
||||
by the regex ``r'\.save'``, which will match messages starting
|
||||
with ``".save"``.
|
||||
|
||||
Inside the method, we check whether the event is replying to another message
|
||||
or not. If it is, we get the reply message and the sender of that message,
|
||||
and download their profile photo.
|
||||
|
||||
Let's delete messages which contain "heck". We don't allow swearing here.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@client.on(events.NewMessage(pattern=r'(?i).*heck'))
|
||||
async def handler(event):
|
||||
await event.delete()
|
||||
|
||||
|
||||
With the ``r'(?i).*heck'`` regex, we match case-insensitive
|
||||
"heck" anywhere in the message. Regex is very powerful and you
|
||||
can learn more at https://regexone.com/.
|
||||
|
||||
So far, we have only seen the `NewMessage
|
||||
<telethon.events.newmessage.NewMessage>`, but there are many more
|
||||
which will be covered later. This is only a small introduction to updates.
|
||||
|
||||
Entities
|
||||
========
|
||||
|
||||
When you need the user or chat where an event occurred, you **must** use
|
||||
the following methods:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def handler(event):
|
||||
# Good
|
||||
chat = await event.get_chat()
|
||||
sender = await event.get_sender()
|
||||
chat_id = event.chat_id
|
||||
sender_id = event.sender_id
|
||||
|
||||
# BAD. Don't do this
|
||||
chat = event.chat
|
||||
sender = event.sender
|
||||
chat_id = event.chat.id
|
||||
sender_id = event.sender.id
|
||||
|
||||
Events are like messages, but don't have all the information a message has!
|
||||
When you manually get a message, it will have all the information it needs.
|
||||
When you receive an update about a message, it **won't** have all the
|
||||
information, so you have to **use the methods**, not the properties.
|
||||
|
||||
Make sure you understand the code seen here before continuing!
|
||||
As a rule of thumb, remember that new message events behave just
|
||||
like message objects, so you can do with them everything you can
|
||||
do with a message object.
|
|
@ -1,368 +0,0 @@
|
|||
.. _mastering-asyncio:
|
||||
|
||||
=================
|
||||
Mastering asyncio
|
||||
=================
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
What's asyncio?
|
||||
===============
|
||||
|
||||
`asyncio` is a Python 3's built-in library. This means it's already installed if
|
||||
you have Python 3. Since Python 3.5, it is convenient to work with asynchronous
|
||||
code. Before (Python 3.4) we didn't have ``async`` or ``await``, but now we do.
|
||||
|
||||
`asyncio` stands for *Asynchronous Input Output*. This is a very powerful
|
||||
concept to use whenever you work IO. Interacting with the web or external
|
||||
APIs such as Telegram's makes a lot of sense this way.
|
||||
|
||||
|
||||
Why asyncio?
|
||||
============
|
||||
|
||||
Asynchronous IO makes a lot of sense in a library like Telethon.
|
||||
You send a request to the server (such as "get some message"), and
|
||||
thanks to `asyncio`, your code won't block while a response arrives.
|
||||
|
||||
The alternative would be to spawn a thread for each update so that
|
||||
other code can run while the response arrives. That is *a lot* more
|
||||
expensive.
|
||||
|
||||
The code will also run faster, because instead of switching back and
|
||||
forth between the OS and your script, your script can handle it all.
|
||||
Avoiding switching saves quite a bit of time, in Python or any other
|
||||
language that supports asynchronous IO. It will also be cheaper,
|
||||
because tasks are smaller than threads, which are smaller than processes.
|
||||
|
||||
|
||||
What are asyncio basics?
|
||||
========================
|
||||
|
||||
The code samples below assume that you have Python 3.7 or greater installed.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# First we need the asyncio library
|
||||
import asyncio
|
||||
|
||||
# We also need something to run
|
||||
async def main():
|
||||
for char in 'Hello, world!\n':
|
||||
print(char, end='', flush=True)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
# Then, we can create a new asyncio loop and use it to run our coroutine.
|
||||
# The creation and tear-down of the loop is hidden away from us.
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
What does telethon.sync do?
|
||||
===========================
|
||||
|
||||
The moment you import any of these:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import sync, ...
|
||||
# or
|
||||
from telethon.sync import ...
|
||||
# or
|
||||
import telethon.sync
|
||||
|
||||
The ``sync`` module rewrites most ``async def``
|
||||
methods in Telethon to something similar to this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def new_method():
|
||||
result = original_method()
|
||||
if loop.is_running():
|
||||
# the loop is already running, return the await-able to the user
|
||||
return result
|
||||
else:
|
||||
# the loop is not running yet, so we can run it for the user
|
||||
return loop.run_until_complete(result)
|
||||
|
||||
|
||||
That means you can do this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(client.get_me().username)
|
||||
|
||||
Instead of this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
me = client.loop.run_until_complete(client.get_me())
|
||||
print(me.username)
|
||||
|
||||
# or, using asyncio's default loop (it's the same)
|
||||
import asyncio
|
||||
loop = asyncio.get_running_loop() # == client.loop
|
||||
me = loop.run_until_complete(client.get_me())
|
||||
print(me.username)
|
||||
|
||||
|
||||
As you can see, it's a lot of boilerplate and noise having to type
|
||||
``run_until_complete`` all the time, so you can let the magic module
|
||||
to rewrite it for you. But notice the comment above: it won't run
|
||||
the loop if it's already running, because it can't. That means this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def main():
|
||||
# 3. the loop is running here
|
||||
print(
|
||||
client.get_me() # 4. this will return a coroutine!
|
||||
.username # 5. this fails, coroutines don't have usernames
|
||||
)
|
||||
|
||||
loop.run_until_complete( # 2. run the loop and the ``main()`` coroutine
|
||||
main() # 1. calling ``async def`` "returns" a coroutine
|
||||
)
|
||||
|
||||
|
||||
Will fail. So if you're inside an ``async def``, then the loop is
|
||||
running, and if the loop is running, you must ``await`` things yourself:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def main():
|
||||
print((await client.get_me()).username)
|
||||
|
||||
loop.run_until_complete(main())
|
||||
|
||||
|
||||
What are async, await and coroutines?
|
||||
=====================================
|
||||
|
||||
The ``async`` keyword lets you define asynchronous functions,
|
||||
also known as coroutines, and also iterate over asynchronous
|
||||
loops or use ``async with``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
# ^ this declares the main() coroutine function
|
||||
|
||||
async with client:
|
||||
# ^ this is an asynchronous with block
|
||||
|
||||
async for message in client.iter_messages(chat):
|
||||
# ^ this is a for loop over an asynchronous generator
|
||||
|
||||
print(message.sender.username)
|
||||
|
||||
asyncio.run(main())
|
||||
# ^ this will create a new asyncio loop behind the scenes and tear it down
|
||||
# once the function returns. It will run the loop untiil main finishes.
|
||||
# You should only use this function if there is no other loop running.
|
||||
|
||||
|
||||
The ``await`` keyword blocks the *current* task, and the loop can run
|
||||
other tasks. Tasks can be thought of as "threads", since many can run
|
||||
concurrently:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
|
||||
async def hello(delay):
|
||||
await asyncio.sleep(delay) # await tells the loop this task is "busy"
|
||||
print('hello') # eventually the loop resumes the code here
|
||||
|
||||
async def world(delay):
|
||||
# the loop decides this method should run first
|
||||
await asyncio.sleep(delay) # await tells the loop this task is "busy"
|
||||
print('world') # eventually the loop finishes all tasks
|
||||
|
||||
async def main():
|
||||
asyncio.create_task(world(2)) # create the world task, passing 2 as delay
|
||||
asyncio.create_task(hello(delay=1)) # another task, but with delay 1
|
||||
await asyncio.sleep(3) # wait for three seconds before exiting
|
||||
|
||||
try:
|
||||
# create a new temporary asyncio loop and use it to run main
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
The same example, but without the comment noise:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
|
||||
async def hello(delay):
|
||||
await asyncio.sleep(delay)
|
||||
print('hello')
|
||||
|
||||
async def world(delay):
|
||||
await asyncio.sleep(delay)
|
||||
print('world')
|
||||
|
||||
async def main():
|
||||
asyncio.create_task(world(2))
|
||||
asyncio.create_task(hello(delay=1))
|
||||
await asyncio.sleep(3)
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
Can I use threads?
|
||||
==================
|
||||
|
||||
Yes, you can, but you must understand that the loops themselves are
|
||||
not thread safe. and you must be sure to know what is happening. The
|
||||
easiest and cleanest option is to use `asyncio.run` to create and manage
|
||||
the new event loop for you:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
async def actual_work():
|
||||
client = TelegramClient(..., loop=loop)
|
||||
... # can use `await` here
|
||||
|
||||
def go():
|
||||
asyncio.run(actual_work())
|
||||
|
||||
threading.Thread(target=go).start()
|
||||
|
||||
|
||||
Generally, **you don't need threads** unless you know what you're doing.
|
||||
Just create another task, as shown above. If you're using the Telethon
|
||||
with a library that uses threads, you must be careful to use `threading.Lock`
|
||||
whenever you use the client, or enable the compatible mode. For that, see
|
||||
:ref:`compatibility-and-convenience`.
|
||||
|
||||
You may have seen this error:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
RuntimeError: There is no current event loop in thread 'Thread-1'.
|
||||
|
||||
It just means you didn't create a loop for that thread. Please refer to
|
||||
the ``asyncio`` documentation to correctly learn how to set the event loop
|
||||
for non-main threads.
|
||||
|
||||
|
||||
client.run_until_disconnected() blocks!
|
||||
=======================================
|
||||
|
||||
All of what `client.run_until_disconnected()
|
||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` does is
|
||||
run the `asyncio`'s event loop until the client is disconnected. That means
|
||||
*the loop is running*. And if the loop is running, it will run all the tasks
|
||||
in it. So if you want to run *other* code, create tasks for it:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
async def clock():
|
||||
while True:
|
||||
print('The time:', datetime.now())
|
||||
await asyncio.sleep(1)
|
||||
|
||||
loop.create_task(clock())
|
||||
...
|
||||
client.run_until_disconnected()
|
||||
|
||||
This creates a task for a clock that prints the time every second.
|
||||
You don't need to use `client.run_until_disconnected()
|
||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` either!
|
||||
You just need to make the loop is running, somehow. `loop.run_forever()
|
||||
<asyncio.loop.run_forever()>` and `loop.run_until_complete()
|
||||
<asyncio.loop.run_until_complete>` can also be used to run
|
||||
the loop, and Telethon will be happy with any approach.
|
||||
|
||||
Of course, there are better tools to run code hourly or daily, see below.
|
||||
|
||||
|
||||
What else can asyncio do?
|
||||
=========================
|
||||
|
||||
Asynchronous IO is a really powerful tool, as we've seen. There are plenty
|
||||
of other useful libraries that also use `asyncio` and that you can integrate
|
||||
with Telethon.
|
||||
|
||||
* `aiohttp <https://github.com/aio-libs/aiohttp>`_ is like the infamous
|
||||
`requests <https://github.com/requests/requests/>`_ but asynchronous.
|
||||
* `quart <https://gitlab.com/pgjones/quart>`_ is an asynchronous alternative
|
||||
to `Flask <http://flask.pocoo.org/>`_.
|
||||
* `aiocron <https://github.com/gawel/aiocron>`_ lets you schedule things
|
||||
to run things at a desired time, or run some tasks hourly, daily, etc.
|
||||
|
||||
And of course, `asyncio <https://docs.python.org/3/library/asyncio.html>`_
|
||||
itself! It has a lot of methods that let you do nice things. For example,
|
||||
you can run requests in parallel:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def main():
|
||||
last, sent, download_path = await asyncio.gather(
|
||||
client.get_messages('telegram', 10),
|
||||
client.send_message('me', 'Using asyncio!'),
|
||||
client.download_profile_photo('telegram')
|
||||
)
|
||||
|
||||
loop.run_until_complete(main())
|
||||
|
||||
|
||||
This code will get the 10 last messages from `@telegram
|
||||
<https://t.me/telegram>`_, send one to the chat with yourself, and also
|
||||
download the profile photo of the channel. `asyncio` will run all these
|
||||
three tasks at the same time. You can run all the tasks you want this way.
|
||||
|
||||
A different way would be:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
loop.create_task(client.get_messages('telegram', 10))
|
||||
loop.create_task(client.send_message('me', 'Using asyncio!'))
|
||||
loop.create_task(client.download_profile_photo('telegram'))
|
||||
|
||||
They will run in the background as long as the loop is running too.
|
||||
|
||||
You can also `start an asyncio server
|
||||
<https://docs.python.org/3/library/asyncio-stream.html#asyncio.start_server>`_
|
||||
in the main script, and from another script, `connect to it
|
||||
<https://docs.python.org/3/library/asyncio-stream.html#asyncio.open_connection>`_
|
||||
to achieve `Inter-Process Communication
|
||||
<https://en.wikipedia.org/wiki/Inter-process_communication>`_.
|
||||
You can get as creative as you want. You can program anything you want.
|
||||
When you use a library, you're not limited to use only its methods. You can
|
||||
combine all the libraries you want. People seem to forget this simple fact!
|
||||
|
||||
|
||||
Why does client.start() work outside async?
|
||||
===========================================
|
||||
|
||||
Because it's so common that it's really convenient to offer said
|
||||
functionality by default. This means you can set up all your event
|
||||
handlers and start the client without worrying about loops at all.
|
||||
|
||||
Using the client in a ``with`` block, `start
|
||||
<telethon.client.auth.AuthMethods.start>`, `run_until_disconnected
|
||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>`, and
|
||||
`disconnect <telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`
|
||||
all support this.
|
||||
|
||||
Where can I read more?
|
||||
======================
|
||||
|
||||
`Check out my blog post
|
||||
<https://lonami.dev/blog/asyncio/>`_ about `asyncio`, which
|
||||
has some more examples and pictures to help you understand what happens
|
||||
when the loop runs.
|
|
@ -1,336 +0,0 @@
|
|||
.. _botapi:
|
||||
|
||||
=======================
|
||||
HTTP Bot API vs MTProto
|
||||
=======================
|
||||
|
||||
|
||||
Telethon is more than just another viable alternative when developing bots
|
||||
for Telegram. If you haven't decided which wrapper library for bots to use
|
||||
yet, using Telethon from the beginning may save you some headaches later.
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
What is Bot API?
|
||||
================
|
||||
|
||||
The `Telegram Bot API`_, also known as HTTP Bot API and from now on referred
|
||||
to as simply "Bot API" is Telegram's official way for developers to control
|
||||
their own Telegram bots. Quoting their main page:
|
||||
|
||||
The Bot API is an HTTP-based interface created for developers keen on
|
||||
building bots for Telegram.
|
||||
|
||||
To learn how to create and set up a bot, please consult our
|
||||
`Introduction to Bots`_ and `Bot FAQ`_.
|
||||
|
||||
Bot API is simply an HTTP endpoint which translates your requests to it into
|
||||
MTProto calls through tdlib_, their bot backend.
|
||||
|
||||
Configuration of your bot, such as its available commands and auto-completion,
|
||||
is configured through `@BotFather <https://t.me/BotFather>`_.
|
||||
|
||||
|
||||
What is MTProto?
|
||||
================
|
||||
|
||||
MTProto_ is Telegram's own protocol to communicate with their API when you
|
||||
connect to their servers.
|
||||
|
||||
Telethon is an alternative MTProto-based backend written entirely in Python
|
||||
and much easier to setup and use.
|
||||
|
||||
Both official applications and third-party clients (like your own
|
||||
applications) logged in as either user or bots **can use MTProto** to
|
||||
communicate directly with Telegram's API (which is not the HTTP bot API).
|
||||
|
||||
When we talk about MTProto, we often mean "MTProto-based clients".
|
||||
|
||||
|
||||
Advantages of MTProto over Bot API
|
||||
==================================
|
||||
|
||||
MTProto clients (like Telethon) connect directly to Telegram's servers,
|
||||
which means there is no HTTP connection, no "polling" or "web hooks". This
|
||||
means **less overhead**, since the protocol used between you and the server
|
||||
is much more compact than HTTP requests with responses in wasteful JSON.
|
||||
|
||||
Since there is a direct connection to Telegram's servers, even if their
|
||||
Bot API endpoint is down, you can still have connection to Telegram directly.
|
||||
|
||||
Using a MTProto client, you are also not limited to the public API that
|
||||
they expose, and instead, **you have full control** of what your bot can do.
|
||||
Telethon offers you all the power with often **much easier usage** than any
|
||||
of the available Python Bot API wrappers.
|
||||
|
||||
If your application ever needs user features because bots cannot do certain
|
||||
things, you will be able to easily login as a user and even keep your bot
|
||||
without having to learn a new library.
|
||||
|
||||
If less overhead and full control didn't convince you to use Telethon yet,
|
||||
check out the wiki page `MTProto vs HTTP Bot API`_ with a more exhaustive
|
||||
and up-to-date list of differences.
|
||||
|
||||
|
||||
Migrating from Bot API to Telethon
|
||||
==================================
|
||||
|
||||
It doesn't matter if you wrote your bot with requests_ and you were
|
||||
making API requests manually, or if you used a wrapper library like
|
||||
python-telegram-bot_ or pyTelegramBotAPI_. It's never too late to
|
||||
migrate to Telethon!
|
||||
|
||||
If you were using an asynchronous library like aiohttp_ or a wrapper like
|
||||
aiogram_ or dumbot_, it will be even easier, because Telethon is also an
|
||||
asynchronous library.
|
||||
|
||||
Next, we will see some examples from the most popular libraries.
|
||||
|
||||
|
||||
Migrating from python-telegram-bot
|
||||
----------------------------------
|
||||
|
||||
Let's take their `echobot.py`_ example and shorten it a bit:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
|
||||
|
||||
def start(update, context):
|
||||
"""Send a message when the command /start is issued."""
|
||||
update.message.reply_text('Hi!')
|
||||
|
||||
def echo(update, context):
|
||||
"""Echo the user message."""
|
||||
update.message.reply_text(update.message.text)
|
||||
|
||||
def main():
|
||||
"""Start the bot."""
|
||||
updater = Updater("TOKEN")
|
||||
dp = updater.dispatcher
|
||||
dp.add_handler(CommandHandler("start", start))
|
||||
dp.add_handler(MessageHandler(Filters.text & ~Filters.command, echo))
|
||||
|
||||
updater.start_polling()
|
||||
|
||||
updater.idle()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
||||
After using Telethon:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, events
|
||||
|
||||
bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN')
|
||||
|
||||
@bot.on(events.NewMessage(pattern='/start'))
|
||||
async def start(event):
|
||||
"""Send a message when the command /start is issued."""
|
||||
await event.respond('Hi!')
|
||||
raise events.StopPropagation
|
||||
|
||||
@bot.on(events.NewMessage)
|
||||
async def echo(event):
|
||||
"""Echo the user message."""
|
||||
await event.respond(event.text)
|
||||
|
||||
def main():
|
||||
"""Start the bot."""
|
||||
bot.run_until_disconnected()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
Key differences:
|
||||
|
||||
* The recommended way to do it imports fewer things.
|
||||
* All handlers trigger by default, so we need ``events.StopPropagation``.
|
||||
* Adding handlers, responding and running is a lot less verbose.
|
||||
* Telethon needs ``async def`` and ``await``.
|
||||
* The ``bot`` isn't hidden away by ``Updater`` or ``Dispatcher``.
|
||||
|
||||
|
||||
Migrating from pyTelegramBotAPI
|
||||
-------------------------------
|
||||
|
||||
Let's show another echobot from their README:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import telebot
|
||||
|
||||
bot = telebot.TeleBot("TOKEN")
|
||||
|
||||
@bot.message_handler(commands=['start'])
|
||||
def send_welcome(message):
|
||||
bot.reply_to(message, "Howdy, how are you doing?")
|
||||
|
||||
@bot.message_handler(func=lambda m: True)
|
||||
def echo_all(message):
|
||||
bot.reply_to(message, message.text)
|
||||
|
||||
bot.polling()
|
||||
|
||||
Now we rewrite it to use Telethon:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, events
|
||||
|
||||
bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN')
|
||||
|
||||
@bot.on(events.NewMessage(pattern='/start'))
|
||||
async def send_welcome(event):
|
||||
await event.reply('Howdy, how are you doing?')
|
||||
|
||||
@bot.on(events.NewMessage)
|
||||
async def echo_all(event):
|
||||
await event.reply(event.text)
|
||||
|
||||
bot.run_until_disconnected()
|
||||
|
||||
Key differences:
|
||||
|
||||
* Instead of doing ``bot.reply_to(message)``, we can do ``event.reply``.
|
||||
Note that the ``event`` behaves just like their ``message``.
|
||||
* Telethon also supports ``func=lambda m: True``, but it's not necessary.
|
||||
|
||||
|
||||
Migrating from aiogram
|
||||
----------------------
|
||||
|
||||
From their GitHub:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from aiogram import Bot, Dispatcher, executor, types
|
||||
|
||||
API_TOKEN = 'BOT TOKEN HERE'
|
||||
|
||||
# Initialize bot and dispatcher
|
||||
bot = Bot(token=API_TOKEN)
|
||||
dp = Dispatcher(bot)
|
||||
|
||||
@dp.message_handler(commands=['start'])
|
||||
async def send_welcome(message: types.Message):
|
||||
"""
|
||||
This handler will be called when client send `/start` command.
|
||||
"""
|
||||
await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.")
|
||||
|
||||
@dp.message_handler(regexp='(^cat[s]?$|puss)')
|
||||
async def cats(message: types.Message):
|
||||
with open('data/cats.jpg', 'rb') as photo:
|
||||
await bot.send_photo(message.chat.id, photo, caption='Cats is here 😺',
|
||||
reply_to_message_id=message.message_id)
|
||||
|
||||
@dp.message_handler()
|
||||
async def echo(message: types.Message):
|
||||
await bot.send_message(message.chat.id, message.text)
|
||||
|
||||
if __name__ == '__main__':
|
||||
executor.start_polling(dp, skip_updates=True)
|
||||
|
||||
|
||||
After rewrite:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, events
|
||||
|
||||
# Initialize bot and... just the bot!
|
||||
bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN')
|
||||
|
||||
@bot.on(events.NewMessage(pattern='/start'))
|
||||
async def send_welcome(event):
|
||||
await event.reply('Howdy, how are you doing?')
|
||||
|
||||
@bot.on(events.NewMessage(pattern='(^cat[s]?$|puss)'))
|
||||
async def cats(event):
|
||||
await event.reply('Cats is here 😺', file='data/cats.jpg')
|
||||
|
||||
@bot.on(events.NewMessage)
|
||||
async def echo_all(event):
|
||||
await event.reply(event.text)
|
||||
|
||||
if __name__ == '__main__':
|
||||
bot.run_until_disconnected()
|
||||
|
||||
|
||||
Key differences:
|
||||
|
||||
* Telethon offers convenience methods to avoid retyping
|
||||
``bot.send_photo(message.chat.id, ...)`` all the time,
|
||||
and instead let you type ``event.reply``.
|
||||
* Sending files is **a lot** easier. The methods for sending
|
||||
photos, documents, audios, etc. are all the same!
|
||||
|
||||
Migrating from dumbot
|
||||
---------------------
|
||||
|
||||
Showcasing their subclassing example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dumbot import Bot
|
||||
|
||||
class Subbot(Bot):
|
||||
async def init(self):
|
||||
self.me = await self.getMe()
|
||||
|
||||
async def on_update(self, update):
|
||||
await self.sendMessage(
|
||||
chat_id=update.message.chat.id,
|
||||
text='i am {}'.format(self.me.username)
|
||||
)
|
||||
|
||||
Subbot(token).run()
|
||||
|
||||
After rewriting:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, events
|
||||
|
||||
class Subbot(TelegramClient):
|
||||
def __init__(self, *a, **kw):
|
||||
super().__init__(*a, **kw)
|
||||
self.add_event_handler(self.on_update, events.NewMessage)
|
||||
|
||||
async def connect():
|
||||
await super().connect()
|
||||
self.me = await self.get_me()
|
||||
|
||||
async def on_update(event):
|
||||
await event.reply('i am {}'.format(self.me.username))
|
||||
|
||||
bot = Subbot('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN')
|
||||
bot.run_until_disconnected()
|
||||
|
||||
|
||||
Key differences:
|
||||
|
||||
* Telethon method names are ``snake_case``.
|
||||
* dumbot does not offer friendly methods like ``update.reply``.
|
||||
* Telethon does not have an implicit ``on_update`` handler, so
|
||||
we need to manually register one.
|
||||
|
||||
|
||||
.. _Telegram Bot API: https://core.telegram.org/bots/api
|
||||
.. _Introduction to Bots: https://core.telegram.org/bots
|
||||
.. _Bot FAQ: https://core.telegram.org/bots/faq
|
||||
.. _tdlib: https://core.telegram.org/tdlib
|
||||
.. _MTProto: https://core.telegram.org/mtproto
|
||||
.. _MTProto vs HTTP Bot API: https://github.com/LonamiWebs/Telethon/wiki/MTProto-vs-HTTP-Bot-API
|
||||
.. _requests: https://pypi.org/project/requests/
|
||||
.. _python-telegram-bot: https://python-telegram-bot.readthedocs.io
|
||||
.. _pyTelegramBotAPI: https://github.com/eternnoir/pyTelegramBotAPI
|
||||
.. _aiohttp: https://docs.aiohttp.org/en/stable
|
||||
.. _aiogram: https://aiogram.readthedocs.io
|
||||
.. _dumbot: https://github.com/Lonami/dumbot
|
||||
.. _echobot.py: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py
|
|
@ -1,169 +0,0 @@
|
|||
.. _chats-channels:
|
||||
|
||||
=================
|
||||
Chats vs Channels
|
||||
=================
|
||||
|
||||
Telegram's raw API can get very confusing sometimes, in particular when it
|
||||
comes to talking about "chats", "channels", "groups", "megagroups", and all
|
||||
those concepts.
|
||||
|
||||
This section will try to explain what each of these concepts are.
|
||||
|
||||
|
||||
Chats
|
||||
=====
|
||||
|
||||
A ``Chat`` can be used to talk about either the common "subclass" that both
|
||||
chats and channels share, or the concrete :tl:`Chat` type.
|
||||
|
||||
Technically, both :tl:`Chat` and :tl:`Channel` are a form of the `Chat type`_.
|
||||
|
||||
**Most of the time**, the term :tl:`Chat` is used to talk about *small group
|
||||
chats*. When you create a group through an official application, this is the
|
||||
type that you get. Official applications refer to these as "Group".
|
||||
|
||||
Both the bot API and Telethon will add a minus sign (negate) the real chat ID
|
||||
so that you can tell at a glance, with just a number, the entity type.
|
||||
|
||||
For example, if you create a chat with :tl:`CreateChatRequest`, the real chat
|
||||
ID might be something like `123`. If you try printing it from a
|
||||
`message.chat_id` you will see `-123`. This ID helps Telethon know you're
|
||||
talking about a :tl:`Chat`.
|
||||
|
||||
|
||||
Channels
|
||||
========
|
||||
|
||||
Official applications create a *broadcast* channel when you create a new
|
||||
channel (used to broadcast messages, only administrators can post messages).
|
||||
|
||||
Official applications implicitly *migrate* an *existing* :tl:`Chat` to a
|
||||
*megagroup* :tl:`Channel` when you perform certain actions (exceed user limit,
|
||||
add a public username, set certain permissions, etc.).
|
||||
|
||||
A ``Channel`` can be created directly with :tl:`CreateChannelRequest`, as
|
||||
either a ``megagroup`` or ``broadcast``.
|
||||
|
||||
Official applications use the term "channel" **only** for broadcast channels.
|
||||
|
||||
The API refers to the different types of :tl:`Channel` with certain attributes:
|
||||
|
||||
* A **broadcast channel** is a :tl:`Channel` with the ``channel.broadcast``
|
||||
attribute set to `True`.
|
||||
|
||||
* A **megagroup channel** is a :tl:`Channel` with the ``channel.megagroup``
|
||||
attribute set to `True`. Official applications refer to this as "supergroup".
|
||||
|
||||
* A **gigagroup channel** is a :tl:`Channel` with the ``channel.gigagroup``
|
||||
attribute set to `True`. Official applications refer to this as "broadcast
|
||||
groups", and is used when a megagroup becomes very large and administrators
|
||||
want to transform it into something where only they can post messages.
|
||||
|
||||
|
||||
Both the bot API and Telethon will "concatenate" ``-100`` to the real chat ID
|
||||
so that you can tell at a glance, with just a number, the entity type.
|
||||
|
||||
For example, if you create a new broadcast channel, the real channel ID might
|
||||
be something like `456`. If you try printing it from a `message.chat_id` you
|
||||
will see `-1000000000456`. This ID helps Telethon know you're talking about a
|
||||
:tl:`Channel`.
|
||||
|
||||
|
||||
Converting IDs
|
||||
==============
|
||||
|
||||
You can convert between the "marked" identifiers (prefixed with a minus sign)
|
||||
and the real ones with ``utils.resolve_id``. It will return a tuple with the
|
||||
real ID, and the peer type (the class):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import utils
|
||||
real_id, peer_type = utils.resolve_id(-1000000000456)
|
||||
|
||||
print(real_id) # 456
|
||||
print(peer_type) # <class 'telethon.tl.types.PeerChannel'>
|
||||
|
||||
peer = peer_type(real_id)
|
||||
print(peer) # PeerChannel(channel_id=456)
|
||||
|
||||
|
||||
The reverse operation can be done with ``utils.get_peer_id``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(utils.get_peer_id(types.PeerChannel(456))) # -1000000000456
|
||||
|
||||
|
||||
Note that this function can also work with other types, like :tl:`Chat` or
|
||||
:tl:`Channel` instances.
|
||||
|
||||
If you need to convert other types like usernames which might need to perform
|
||||
API calls to find out the identifier, you can use ``client.get_peer_id``:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(await client.get_peer_id('me')) # your id
|
||||
|
||||
|
||||
If there is no "mark" (no minus sign), Telethon will assume your identifier
|
||||
refers to a :tl:`User`. If this is **not** the case, you can manually fix it:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import types
|
||||
await client.send_message(types.PeerChannel(456), 'hello')
|
||||
# ^^^^^^^^^^^^^^^^^ explicit peer type
|
||||
|
||||
|
||||
A note on raw API
|
||||
=================
|
||||
|
||||
Certain methods only work on a :tl:`Chat`, and some others only work on a
|
||||
:tl:`Channel` (and these may only work in broadcast, or megagroup). Your code
|
||||
likely knows what it's working with, so it shouldn't be too much of an issue.
|
||||
|
||||
If you need to find the :tl:`Channel` from a :tl:`Chat` that migrated to it,
|
||||
access the `migrated_to` property:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# chat is a Chat
|
||||
channel = await client.get_entity(chat.migrated_to)
|
||||
# channel is now a Channel
|
||||
|
||||
Channels do not have a "migrated_from", but a :tl:`ChannelFull` does. You can
|
||||
use :tl:`GetFullChannelRequest` to obtain this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import functions
|
||||
full = await client(functions.channels.GetFullChannelRequest(your_channel))
|
||||
full_channel = full.full_chat
|
||||
# full_channel is a ChannelFull
|
||||
print(full_channel.migrated_from_chat_id)
|
||||
|
||||
This way, you can also access the linked discussion megagroup of a broadcast channel:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(full_channel.linked_chat_id) # prints ID of linked discussion group or None
|
||||
|
||||
You do not need to use ``client.get_entity`` to access the
|
||||
``migrated_from_chat_id`` :tl:`Chat` or the ``linked_chat_id`` :tl:`Channel`.
|
||||
They are in the ``full.chats`` attribute:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
if full_channel.migrated_from_chat_id:
|
||||
migrated_from_chat = next(c for c in full.chats if c.id == full_channel.migrated_from_chat_id)
|
||||
print(migrated_from_chat.title)
|
||||
|
||||
if full_channel.linked_chat_id:
|
||||
linked_group = next(c for c in full.chats if c.id == full_channel.linked_chat_id)
|
||||
print(linked_group.username)
|
||||
|
||||
.. _Chat type: https://tl.telethon.dev/types/chat.html
|
|
@ -1,313 +0,0 @@
|
|||
.. _entities:
|
||||
|
||||
========
|
||||
Entities
|
||||
========
|
||||
|
||||
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 contact list**.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
What is an Entity?
|
||||
==================
|
||||
|
||||
A lot of methods and requests require *entities* to work. For example,
|
||||
you send a message to an *entity*, get the username of an *entity*, and
|
||||
so on.
|
||||
|
||||
There are a lot of things that work as entities: usernames, phone numbers,
|
||||
chat links, invite links, IDs, and the types themselves. That is, you can
|
||||
use any of those when you see an "entity" is needed.
|
||||
|
||||
.. note::
|
||||
|
||||
Remember that the phone number must be in your contact list before you
|
||||
can use it.
|
||||
|
||||
You should use, **from better to worse**:
|
||||
|
||||
1. Input entities. For example, `event.input_chat
|
||||
<telethon.tl.custom.chatgetter.ChatGetter.input_chat>`,
|
||||
`message.input_sender
|
||||
<telethon.tl.custom.sendergetter.SenderGetter.input_sender>`,
|
||||
or caching an entity you will use a lot with
|
||||
``entity = await client.get_input_entity(...)``.
|
||||
|
||||
2. Entities. For example, if you had to get someone's
|
||||
username, you can just use ``user`` or ``channel``.
|
||||
It will work. Only use this option if you already have the entity!
|
||||
|
||||
3. IDs. This will always look the entity up from the
|
||||
cache (the ``*.session`` file caches seen entities).
|
||||
|
||||
4. Usernames, phone numbers and links. The cache will be
|
||||
used too (unless you force a `client.get_entity()
|
||||
<telethon.client.users.UserMethods.get_entity>`),
|
||||
but may make a request if the username, phone or link
|
||||
has not been found yet.
|
||||
|
||||
In recent versions of the library, the following two are equivalent:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def handler(event):
|
||||
await client.send_message(event.sender_id, 'Hi')
|
||||
await client.send_message(event.input_sender, 'Hi')
|
||||
|
||||
|
||||
If you need to be 99% sure that the code will work (sometimes it's
|
||||
simply impossible for the library to find the input entity), or if
|
||||
you will reuse the chat a lot, consider using the following instead:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def handler(event):
|
||||
# This method may make a network request to find the input sender.
|
||||
# Properties can't make network requests, so we need a method.
|
||||
sender = await event.get_input_sender()
|
||||
await client.send_message(sender, 'Hi')
|
||||
await client.send_message(sender, 'Hi')
|
||||
|
||||
|
||||
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
|
||||
|
||||
# (These examples assume you are inside an "async def")
|
||||
#
|
||||
# Dialogs are the "conversations you have open".
|
||||
# This method returns a list of Dialog, which
|
||||
# has the .entity attribute and other information.
|
||||
#
|
||||
# This part is IMPORTANT, because it fills the entity cache.
|
||||
dialogs = await client.get_dialogs()
|
||||
|
||||
# All of these work and do the same.
|
||||
username = await client.get_entity('username')
|
||||
username = await client.get_entity('t.me/username')
|
||||
username = await client.get_entity('https://telegram.dog/username')
|
||||
|
||||
# Other kind of entities.
|
||||
channel = await client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg')
|
||||
contact = await client.get_entity('+34xxxxxxxxx')
|
||||
friend = await client.get_entity(friend_id)
|
||||
|
||||
# Getting entities through their ID (User, Chat or Channel)
|
||||
entity = await 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 = await client.get_entity(PeerUser(some_id))
|
||||
my_chat = await client.get_entity(PeerChat(some_id))
|
||||
my_channel = await client.get_entity(PeerChannel(some_id))
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
You **don't** need to get the entity before using it! Just let the
|
||||
library do its job. Use a phone from your contacts, username, ID or
|
||||
input entity (preferred but not necessary), whatever you already have.
|
||||
|
||||
All methods in the :ref:`telethon-client` call `.get_input_entity()
|
||||
<telethon.client.users.UserMethods.get_input_entity>` prior
|
||||
to sending the request to save you from the hassle of doing so manually.
|
||||
That way, convenience calls such as `client.send_message('username', '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::
|
||||
|
||||
This section is informative, but worth reading. The library
|
||||
will transparently handle all of these details for you.
|
||||
|
||||
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**. They
|
||||
are named like this because they are input parameters in the requests.
|
||||
|
||||
Entities' ID are the same for all user and bot accounts, however, the access
|
||||
hash is **different for each account**, so trying to reuse the access hash
|
||||
from one account in another will **not** work.
|
||||
|
||||
Sometimes, Telegram only needs to indicate the type of the entity along
|
||||
with their ID. For this purpose, :tl:`Peer` versions of the entities also
|
||||
exist, which just have the ID. You cannot get the hash out of them since
|
||||
you should not be needing it. The library probably has cached it before.
|
||||
|
||||
Peers are enough to identify an entity, but they are not enough to make
|
||||
a request with them. You need to know their hash before you can
|
||||
"use them", and to know the hash you need to "encounter" them, let it
|
||||
be in your dialogs, participants, message forwards, etc.
|
||||
|
||||
.. note::
|
||||
|
||||
You *can* use peers with the library. Behind the scenes, they are
|
||||
replaced with the input variant. Peers "aren't enough" on their own
|
||||
but the library will do some more work to use the right type.
|
||||
|
||||
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
|
||||
|
||||
await 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.
|
||||
|
||||
|
||||
Accessing Entities
|
||||
==================
|
||||
|
||||
Although it's explicitly noted in the documentation that messages
|
||||
*subclass* `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`
|
||||
and `SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>`,
|
||||
some people still don't get inheritance.
|
||||
|
||||
When the documentation says "Bases: `telethon.tl.custom.chatgetter.ChatGetter`"
|
||||
it means that the class you're looking at, *also* can act as the class it
|
||||
bases. In this case, `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`
|
||||
knows how to get the *chat* where a thing belongs to.
|
||||
|
||||
So, a `Message <telethon.tl.custom.message.Message>` is a
|
||||
`ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`.
|
||||
That means you can do this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
message.is_private
|
||||
message.chat_id
|
||||
await message.get_chat()
|
||||
# ...etc
|
||||
|
||||
`SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>` is similar:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
message.user_id
|
||||
await message.get_input_user()
|
||||
message.user
|
||||
# ...etc
|
||||
|
||||
Quite a few things implement them, so it makes sense to reuse the code.
|
||||
For example, all events (except raw updates) implement `ChatGetter
|
||||
<telethon.tl.custom.chatgetter.ChatGetter>` since all events occur
|
||||
in some chat.
|
||||
|
||||
|
||||
Summary
|
||||
=======
|
||||
|
||||
TL;DR; If you're here because of *"Could not find the input entity for"*,
|
||||
you must ask yourself "how did I find this entity through official
|
||||
applications"? Now do the same with the library. Use what applies:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# (These examples assume you are inside an "async def")
|
||||
async with client:
|
||||
# Does it have a username? Use it!
|
||||
entity = await client.get_entity(username)
|
||||
|
||||
# Do you have a conversation open with them? Get dialogs.
|
||||
await client.get_dialogs()
|
||||
|
||||
# Are they participant of some group? Get them.
|
||||
await client.get_participants('username')
|
||||
|
||||
# Is the entity the original sender of a forwarded message? Get it.
|
||||
await client.get_messages('username', 100)
|
||||
|
||||
# NOW you can use the ID, anywhere!
|
||||
await client.send_message(123456, 'Hi!')
|
||||
|
||||
entity = await client.get_entity(123456)
|
||||
print(entity)
|
||||
|
||||
Once the library has "seen" the entity, you can use their **integer** ID.
|
||||
You can't use entities from IDs the library hasn't seen. You must make the
|
||||
library see them *at least once* and disconnect properly. You know where
|
||||
the entities are and you must tell the library. It won't guess for you.
|
|
@ -1,155 +0,0 @@
|
|||
.. _rpc-errors:
|
||||
|
||||
==========
|
||||
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).
|
||||
|
||||
You should import the errors from ``telethon.errors`` like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import errors
|
||||
|
||||
try:
|
||||
async with client.takeout() as takeout:
|
||||
...
|
||||
|
||||
except errors.TakeoutInitDelayError as e:
|
||||
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ here we except TAKEOUT_INIT_DELAY
|
||||
print('Must wait', e.seconds, 'before takeout')
|
||||
|
||||
|
||||
There isn't any official list of all possible RPC errors, so the
|
||||
`list of known errors`_ is provided on a best-effort basis. When new methods
|
||||
are available, the list may be lacking since we simply don't know what errors
|
||||
can raise from them.
|
||||
|
||||
Once we do find out about a new error and what causes it, the list is
|
||||
updated, so if you see an error without a specific class, do report it
|
||||
(and what method caused it)!.
|
||||
|
||||
This list is used to generate documentation for the `raw API page`_.
|
||||
For example, if we want to know what errors can occur from
|
||||
`messages.sendMessage`_ we can simply navigate to its raw API page
|
||||
and find it has 24 known RPC errors at the time of writing.
|
||||
|
||||
|
||||
Base Errors
|
||||
===========
|
||||
|
||||
All the "base" errors are listed in :ref:`telethon-errors`.
|
||||
Any other more specific error will be a subclass of these.
|
||||
|
||||
If the library isn't aware of a specific error just yet, it will instead
|
||||
raise one of these superclasses. This means you may find stuff like this:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
telethon.errors.rpcbaseerrors.BadRequestError: RPCError 400: MESSAGE_POLL_CLOSED (caused by SendVoteRequest)
|
||||
|
||||
If you do, make sure to open an issue or send a pull request to update the
|
||||
`list of known errors`_.
|
||||
|
||||
|
||||
Common Errors
|
||||
=============
|
||||
|
||||
These are some of the errors you may normally need to deal with:
|
||||
|
||||
- ``FloodWaitError`` (420), the same request was repeated many times.
|
||||
Must wait ``.seconds`` (you can access this attribute). For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
...
|
||||
from telethon import errors
|
||||
|
||||
try:
|
||||
messages = await client.get_messages(chat)
|
||||
print(messages[0].text)
|
||||
except errors.FloodWaitError as e:
|
||||
print('Have to sleep', e.seconds, 'seconds')
|
||||
time.sleep(e.seconds)
|
||||
|
||||
- ``SessionPasswordNeededError``, if you have setup two-steps
|
||||
verification on Telegram and are trying to sign in.
|
||||
- ``FilePartMissingError``, if you have tried to upload an empty file.
|
||||
- ``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``.
|
||||
|
||||
You can refer to all errors from Python through the ``telethon.errors``
|
||||
module. If you don't know what attributes they have, try printing their
|
||||
dir (like ``print(dir(e))``).
|
||||
|
||||
|
||||
Attributes
|
||||
==========
|
||||
|
||||
Some of the errors carry additional data in them. When they look like
|
||||
``EMAIL_UNCONFIRMED_X``, the ``_X`` value will be accessible from the
|
||||
error instance. The current list of errors that do this is the following:
|
||||
|
||||
- ``EmailUnconfirmedError`` has ``.code_length``.
|
||||
- ``FileMigrateError`` has ``.new_dc``.
|
||||
- ``FilePartMissingError`` has ``.which``.
|
||||
- ``FloodTestPhoneWaitError`` has ``.seconds``.
|
||||
- ``FloodWaitError`` has ``.seconds``.
|
||||
- ``InterdcCallErrorError`` has ``.dc``.
|
||||
- ``InterdcCallRichErrorError`` has ``.dc``.
|
||||
- ``NetworkMigrateError`` has ``.new_dc``.
|
||||
- ``PhoneMigrateError`` has ``.new_dc``.
|
||||
- ``SlowModeWaitError`` has ``.seconds``.
|
||||
- ``TakeoutInitDelayError`` has ``.seconds``.
|
||||
- ``UserMigrateError`` has ``.new_dc``.
|
||||
|
||||
|
||||
Avoiding Limits
|
||||
===============
|
||||
|
||||
Don't spam. You won't get ``FloodWaitError`` or your account banned or
|
||||
deleted if you use the library *for legit use cases*. Make cool tools.
|
||||
Don't spam! Nobody knows the exact limits for all requests since they
|
||||
depend on a lot of factors, so don't bother asking.
|
||||
|
||||
Still, if you do have a legit use case and still get those errors, the
|
||||
library will automatically sleep when they are smaller than 60 seconds
|
||||
by default. You can set different "auto-sleep" thresholds:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client.flood_sleep_threshold = 0 # Don't auto-sleep
|
||||
client.flood_sleep_threshold = 24 * 60 * 60 # Sleep always
|
||||
|
||||
You can also except it and act as you prefer:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.errors import FloodWaitError
|
||||
try:
|
||||
...
|
||||
except FloodWaitError as e:
|
||||
print('Flood waited for', e.seconds)
|
||||
quit(1)
|
||||
|
||||
VoIP numbers are very limited, and some countries are more limited too.
|
||||
|
||||
|
||||
.. _list of known errors: https://github.com/LonamiWebs/Telethon/blob/v1/telethon_generator/data/errors.csv
|
||||
.. _raw API page: https://tl.telethon.dev/
|
||||
.. _messages.sendMessage: https://tl.telethon.dev/methods/messages/send_message.html
|
|
@ -1,420 +0,0 @@
|
|||
.. _full-api:
|
||||
|
||||
============
|
||||
The Full API
|
||||
============
|
||||
|
||||
.. important::
|
||||
|
||||
While you have access to this, you should always use the friendly
|
||||
methods listed on :ref:`client-ref` unless you have a better reason
|
||||
not to, like a method not existing or you wanting more control.
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
The :ref:`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 defined in Telegram's API.
|
||||
|
||||
This section will teach you how to use what Telethon calls the `TL reference`_.
|
||||
The linked page contains a list and a way to search through *all* types
|
||||
generated from the definition of Telegram's API (in ``.tl`` file format,
|
||||
hence the name). These types include requests and constructors.
|
||||
|
||||
.. note::
|
||||
|
||||
The reason to keep both https://tl.telethon.dev 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.
|
||||
|
||||
Telegram makes these ``.tl`` files public, which other implementations, such
|
||||
as Telethon, can also use to generate code. These files are versioned under
|
||||
what's called "layers". ``.tl`` files consist of thousands of definitions,
|
||||
and newer layers often add, change, or remove them. Each definition refers
|
||||
to either a Remote Procedure Call (RPC) function, or a type (which the
|
||||
`TL reference`_ calls "constructors", as they construct particular type
|
||||
instances).
|
||||
|
||||
As such, the `TL reference`_ is a good place to go to learn about all possible
|
||||
requests, types, and what they look like. If you're curious about what's been
|
||||
changed between layers, you can refer to the `TL diff`_ site.
|
||||
|
||||
|
||||
Navigating the TL reference
|
||||
===========================
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
"Functions" is the term used for the Remote Procedure Calls (RPC) that can be
|
||||
sent to Telegram to ask it to perform something (e.g. "send message"). These
|
||||
requests have an associated return type. These can be invoked ("called"):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
client = TelegramClient(...)
|
||||
function_instance = SomeRequest(...)
|
||||
|
||||
# Invoke the request
|
||||
returned_type = await client(function_instance)
|
||||
|
||||
Whenever you find the type for a function in the `TL reference`_, the page
|
||||
will contain the following information:
|
||||
|
||||
* What type of account can use the method. This information is regenerated
|
||||
from time to time (by attempting to invoke the function under both account
|
||||
types and finding out where it fails). Some requests can only be used by
|
||||
bot accounts, others by user accounts, and others by both.
|
||||
* The TL definition. This helps you get a feel for the what the function
|
||||
looks like. This is not Python code. It just contains the definition in
|
||||
a concise manner.
|
||||
* "Copy import" button. Does what it says: it will copy the necessary Python
|
||||
code to import the function to your system's clipboard for easy access.
|
||||
* Returns. The returned type. When you invoke the function, this is what the
|
||||
result will be. It also includes which of the constructors can be returned
|
||||
inline, to save you a click.
|
||||
* Parameters. The parameters accepted by the function, including their type,
|
||||
whether they expect a list, and whether they're optional.
|
||||
* Known RPC errors. A best-effort list of known errors the request may cause.
|
||||
This list is not complete and may be out of date, but should provide an
|
||||
overview of what could go wrong.
|
||||
* Example. Autogenerated example, showcasing how you may want to call it.
|
||||
Bear in mind that this is *autogenerated*. It may be spitting out non-sense.
|
||||
The goal of this example is not to show you everything you can do with the
|
||||
request, only to give you a feel for what it looks like to use it.
|
||||
|
||||
It is very important to click through the links and navigate to get the full
|
||||
picture. A specific page will show you what the specific function returns and
|
||||
needs as input parameters. But it may reference other types, so you need to
|
||||
navigate to those to learn what those contain or need.
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
"Types" as understood by TL are not actually generated in Telethon.
|
||||
They would be the "abstract base class" of the constructors, but since Python
|
||||
is duck-typed, there is hardly any need to generate mostly unnecessary code.
|
||||
The page for a type contains:
|
||||
|
||||
* Constructors. Every type will have one or more constructors. These
|
||||
constructors *are* generated and can be immported and used.
|
||||
* Requests returning this type. A helpful way to find out "what requests can
|
||||
return this?". This is how you may learn what request you need to use to
|
||||
obtain a particular instance of a type.
|
||||
* Requests accepting this type as input. A helpful way to find out "what
|
||||
requests can use this type as one of their input parameters?". This is how
|
||||
you may learn where a type is used.
|
||||
* Other types containing this type. A helpful way to find out "where else
|
||||
does this type appear?". This is how you can walk back through nested
|
||||
objects.
|
||||
|
||||
Constructors
|
||||
------------
|
||||
|
||||
Constructors are used to create instances of a particular type, and are also
|
||||
returned when invoking requests. You will have to create instances yourself
|
||||
when invoking requests that need a particular type as input.
|
||||
The page for a constructor contains:
|
||||
|
||||
* Belongs to. The parent type. This is a link back to the types page for the
|
||||
specific constructor. It also contains the sibling constructors inline, to
|
||||
save you a click.
|
||||
* Members. Both the input parameters *and* fields the constructor contains.
|
||||
|
||||
|
||||
Using the TL reference
|
||||
======================
|
||||
|
||||
After you've found a request you want to send, a good start would be to simply
|
||||
copy and paste the autogenerated example into your script. Then you can simply
|
||||
tweak it to your needs.
|
||||
|
||||
If you want to do it from scratch, first, make sure to import the request into
|
||||
your code (either using the "Copy import" button near the top, or by manually
|
||||
spelling out the package under ``telethon.tl.functions.*``).
|
||||
|
||||
Then, start reading the parameters one by one. If the parameter cannot be
|
||||
omitted, you **will** need to specify it, so make sure to spell it out as
|
||||
an input parameter when constructing the request instance. Let's look at
|
||||
`PingRequest`_ for example. First, we copy the import:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions import PingRequest
|
||||
|
||||
Then, we look at the parameters:
|
||||
|
||||
ping_id - long
|
||||
|
||||
A single parameter, and it's a long (a integer number with a large range of
|
||||
values). It doesn't say it can be omitted, so we must provide it, like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
PingRequest(
|
||||
ping_id=48641868471
|
||||
)
|
||||
|
||||
(In this case, the ping ID is a random number. You often have to guess what
|
||||
the parameter needs just by looking at the name.)
|
||||
|
||||
Now that we have our request, we can invoke it:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
response = await client(PingRequest(
|
||||
ping_id=48641868471
|
||||
))
|
||||
|
||||
To find out what ``response`` looks like, we can do as the autogenerated
|
||||
example suggests and "stringify" the result as a pretty-printed string:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(result.stringify())
|
||||
|
||||
This will print out the following:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Pong(
|
||||
msg_id=781875678118,
|
||||
ping_id=48641868471
|
||||
)
|
||||
|
||||
Which is a very easy way to get a feel for a response. You should nearly
|
||||
always print the stringified result, at least once, when trying out requests,
|
||||
to get a feel for what the response may look like.
|
||||
|
||||
But of course, you don't need to do that. Without writing any code, you could
|
||||
have navigated through the "Returns" link to learn ``PingRequest`` returns a
|
||||
``Pong``, which only has one constructor, and the constructor has two members,
|
||||
``msg_id`` and ``ping_id``.
|
||||
|
||||
If you wanted to create your own ``Pong``, you would use both members as input
|
||||
parameters:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
my_pong = Pong(
|
||||
msg_id=781875678118,
|
||||
ping_id=48641868471
|
||||
)
|
||||
|
||||
(Yes, constructing object instances can use the same code that ``.stringify``
|
||||
would return!)
|
||||
|
||||
And if you wanted to access the ``msg_id`` member, you would simply access it
|
||||
like any other attribute access in Python:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(response.msg_id)
|
||||
|
||||
|
||||
Example walkthrough
|
||||
===================
|
||||
|
||||
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
|
||||
|
||||
async def main():
|
||||
peer = await client.get_input_entity('someone')
|
||||
|
||||
client.loop.run_until_complete(main())
|
||||
|
||||
.. note::
|
||||
|
||||
Remember that ``await`` must occur inside an ``async def``.
|
||||
Every full API example assumes you already know and do this.
|
||||
|
||||
|
||||
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 = await 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 = await client(SendMessageRequest(peer, 'Hello there!'))
|
||||
|
||||
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 = await client(SendMessageRequest(
|
||||
await client.get_input_entity('username'), 'Hello there!'
|
||||
))
|
||||
|
||||
|
||||
This can further be simplified to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
result = await client(SendMessageRequest('username', 'Hello there!'))
|
||||
# Or even
|
||||
result = await 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.
|
||||
|
||||
|
||||
Requests in Parallel
|
||||
====================
|
||||
|
||||
The library will automatically merge outgoing requests into a single
|
||||
*container*. Telegram's API supports sending multiple requests in a
|
||||
single container, which is faster because it has less overhead and
|
||||
the server can run them without waiting for others. You can also
|
||||
force using a container manually:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def main():
|
||||
|
||||
# Letting the library do it behind the scenes
|
||||
await asyncio.wait([
|
||||
client.send_message('me', 'Hello'),
|
||||
client.send_message('me', ','),
|
||||
client.send_message('me', 'World'),
|
||||
client.send_message('me', '.')
|
||||
])
|
||||
|
||||
# Manually invoking many requests at once
|
||||
await client([
|
||||
SendMessageRequest('me', 'Hello'),
|
||||
SendMessageRequest('me', ', '),
|
||||
SendMessageRequest('me', 'World'),
|
||||
SendMessageRequest('me', '.')
|
||||
])
|
||||
|
||||
Note that you cannot guarantee the order in which they are run.
|
||||
Try running the above code more than one time. You will see the
|
||||
order in which the messages arrive is different.
|
||||
|
||||
If you use the raw API (the first option), you can use ``ordered``
|
||||
to tell the server that it should run the requests sequentially.
|
||||
This will still be faster than going one by one, since the server
|
||||
knows all requests directly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
await client([
|
||||
SendMessageRequest('me', 'Hello'),
|
||||
SendMessageRequest('me', ', '),
|
||||
SendMessageRequest('me', 'World'),
|
||||
SendMessageRequest('me', '.')
|
||||
], ordered=True)
|
||||
|
||||
If any of the requests fails with a Telegram error (not connection
|
||||
errors or any other unexpected events), the library will raise
|
||||
`telethon.errors.common.MultiError`. You can ``except`` this
|
||||
and still access the successful results:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.errors import MultiError
|
||||
|
||||
try:
|
||||
await client([
|
||||
SendMessageRequest('me', 'Hello'),
|
||||
SendMessageRequest('me', ''),
|
||||
SendMessageRequest('me', 'World')
|
||||
], ordered=True)
|
||||
except MultiError as e:
|
||||
# The first and third requests worked.
|
||||
first = e.results[0]
|
||||
third = e.results[2]
|
||||
# The second request failed.
|
||||
second = e.exceptions[1]
|
||||
|
||||
.. _TL reference: https://tl.telethon.dev
|
||||
.. _TL diff: https://diff.telethon.dev
|
||||
.. _PingRequest: https://tl.telethon.dev/methods/ping.html
|
||||
.. _use the search: https://tl.telethon.dev/?q=message&redirect=no
|
|
@ -1,165 +0,0 @@
|
|||
.. _sessions:
|
||||
|
||||
==============
|
||||
Session Files
|
||||
==============
|
||||
|
||||
.. contents::
|
||||
|
||||
They are an important part for the library to be efficient, such as caching
|
||||
and handling your authorization key (or you would have to login every time!).
|
||||
|
||||
What are Sessions?
|
||||
==================
|
||||
|
||||
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 a 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``.
|
||||
|
||||
|
||||
Different 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.
|
||||
|
||||
While it's often not the case, it's possible that SQLite is slow enough to
|
||||
be noticeable, in which case you can also use a different storage. Note that
|
||||
this is rare and most people won't have this issue, but it's worth a mention.
|
||||
|
||||
To use a custom session storage, simply pass the custom session instance to
|
||||
:ref:`TelegramClient <telethon-client>` instead of
|
||||
the session name.
|
||||
|
||||
Telethon contains three implementations of the abstract ``Session`` class:
|
||||
|
||||
.. currentmodule:: telethon.sessions
|
||||
|
||||
* `MemorySession <memory.MemorySession>`: stores session data within memory.
|
||||
* `SQLiteSession <sqlite.SQLiteSession>`: stores sessions within on-disk SQLite databases. Default.
|
||||
* `StringSession <string.StringSession>`: stores session data within memory,
|
||||
but can be saved as a string.
|
||||
|
||||
You can import these ``from telethon.sessions``. For example, using the
|
||||
`StringSession <string.StringSession>` is done as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.sync import TelegramClient
|
||||
from telethon.sessions import StringSession
|
||||
|
||||
with TelegramClient(StringSession(string), api_id, api_hash) as client:
|
||||
... # use the client
|
||||
|
||||
# Save the string session as a string; you should decide how
|
||||
# you want to save this information (over a socket, remote
|
||||
# database, print it and then paste the string in the code,
|
||||
# etc.); the advantage is that you don't need to save it
|
||||
# on the current disk as a separate file, and can be reused
|
||||
# anywhere else once you log in.
|
||||
string = client.session.save()
|
||||
|
||||
# Note that it's also possible to save any other session type
|
||||
# as a string by using ``StringSession.save(session_instance)``:
|
||||
client = TelegramClient('sqlite-session', api_id, api_hash)
|
||||
string = StringSession.save(client.session)
|
||||
|
||||
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.
|
||||
|
||||
* `MongoDB <https://github.com/watzon/telethon-session-mongo>`_:
|
||||
stores the current session in a MongoDB database.
|
||||
|
||||
|
||||
Creating your Own Storage
|
||||
=========================
|
||||
|
||||
The easiest way to create your own storage implementation is to use
|
||||
`MemorySession <memory.MemorySession>` as the base and check out how
|
||||
`SQLiteSession <sqlite.SQLiteSession>` or one of the community-maintained
|
||||
implementations work. You can find the relevant Python files under the
|
||||
``sessions/`` directory in the Telethon's repository.
|
||||
|
||||
After you have made your own implementation, you can add it to the
|
||||
community-maintained session implementation list above with a pull request.
|
||||
|
||||
|
||||
String Sessions
|
||||
===============
|
||||
|
||||
`StringSession <string.StringSession>` are a convenient way to embed your
|
||||
login credentials directly into your code for extremely easy portability,
|
||||
since all they take is a string to be able to login without asking for your
|
||||
phone and code (or faster start if you're using a bot token).
|
||||
|
||||
The easiest way to generate a string session is as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.sync import TelegramClient
|
||||
from telethon.sessions import StringSession
|
||||
|
||||
with TelegramClient(StringSession(), api_id, api_hash) as client:
|
||||
print(client.session.save())
|
||||
|
||||
|
||||
Think of this as a way to export your authorization key (what's needed
|
||||
to login into your account). This will print a string in the standard
|
||||
output (likely your terminal).
|
||||
|
||||
.. warning::
|
||||
|
||||
**Keep this string safe!** Anyone with this string can use it
|
||||
to login into your account and do anything they want to.
|
||||
|
||||
This is similar to leaking your ``*.session`` files online,
|
||||
but it is easier to leak a string than it is to leak a file.
|
||||
|
||||
|
||||
Once you have the string (which is a bit long), load it into your script
|
||||
somehow. You can use a normal text file and ``open(...).read()`` it or
|
||||
you can save it in a variable directly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
string = '1aaNk8EX-YRfwoRsebUkugFvht6DUPi_Q25UOCzOAqzc...'
|
||||
with TelegramClient(StringSession(string), api_id, api_hash) as client:
|
||||
client.loop.run_until_complete(client.send_message('me', 'Hi'))
|
||||
|
||||
|
||||
These strings are really convenient for using in places like Heroku since
|
||||
their ephemeral filesystem will delete external files once your application
|
||||
is over.
|
|
@ -1,88 +0,0 @@
|
|||
======================
|
||||
String-based Debugging
|
||||
======================
|
||||
|
||||
Debugging is *really* important. Telegram's API is really big and there
|
||||
are a lot of things that you should know. Such as, what attributes or fields
|
||||
does a result have? Well, the easiest thing to do is printing it:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
entity = await client.get_entity('username')
|
||||
print(entity)
|
||||
|
||||
That will show a huge **string** similar to the following:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Channel(id=1066197625, title='Telegram Usernames', photo=ChatPhotoEmpty(), date=datetime.datetime(2016, 12, 16, 15, 15, 43, tzinfo=datetime.timezone.utc), version=0, creator=False, left=True, broadcast=True, verified=True, megagroup=False, restricted=False, signatures=False, min=False, scam=False, has_link=False, has_geo=False, slowmode_enabled=False, access_hash=-6309373984955162244, username='username', restriction_reason=[], admin_rights=None, banned_rights=None, default_banned_rights=None, participants_count=None)
|
||||
|
||||
That's a lot of text. But as you can see, all the properties are there.
|
||||
So if you want the title you **don't use regex** or anything like
|
||||
splitting ``str(entity)`` to get what you want. You just access the
|
||||
attribute you need:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
title = entity.title
|
||||
|
||||
Can we get better than the shown string, though? Yes!
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(entity.stringify())
|
||||
|
||||
Will show a much better representation:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Channel(
|
||||
id=1066197625,
|
||||
title='Telegram Usernames',
|
||||
photo=ChatPhotoEmpty(
|
||||
),
|
||||
date=datetime.datetime(2016, 12, 16, 15, 15, 43, tzinfo=datetime.timezone.utc),
|
||||
version=0,
|
||||
creator=False,
|
||||
left=True,
|
||||
broadcast=True,
|
||||
verified=True,
|
||||
megagroup=False,
|
||||
restricted=False,
|
||||
signatures=False,
|
||||
min=False,
|
||||
scam=False,
|
||||
has_link=False,
|
||||
has_geo=False,
|
||||
slowmode_enabled=False,
|
||||
access_hash=-6309373984955162244,
|
||||
username='username',
|
||||
restriction_reason=[
|
||||
],
|
||||
admin_rights=None,
|
||||
banned_rights=None,
|
||||
default_banned_rights=None,
|
||||
participants_count=None
|
||||
)
|
||||
|
||||
|
||||
Now it's easy to see how we could get, for example,
|
||||
the ``year`` value. It's inside ``date``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
channel_year = entity.date.year
|
||||
|
||||
You don't need to print everything to see what all the possible values
|
||||
can be. You can just search in http://tl.telethon.dev/.
|
||||
|
||||
Remember that you can use Python's `isinstance
|
||||
<https://docs.python.org/3/library/functions.html#isinstance>`_
|
||||
to check the type of something. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import types
|
||||
|
||||
if isinstance(entity.photo, types.ChatPhotoEmpty):
|
||||
print('Channel has no photo')
|
|
@ -1,228 +0,0 @@
|
|||
================
|
||||
Updates in Depth
|
||||
================
|
||||
|
||||
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 the client
|
||||
=========================
|
||||
|
||||
The code of your application starts getting big, so you decide to
|
||||
separate the handlers into different files. But how can you access
|
||||
the client from these files? You don't need to! Just `events.register
|
||||
<telethon.events.register>` them:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# handlers/welcome.py
|
||||
from telethon import events
|
||||
|
||||
@events.register(events.NewMessage('(?i)hello'))
|
||||
async def handler(event):
|
||||
client = event.client
|
||||
await event.respond('Hey!')
|
||||
await client.send_message('me', 'I said hello to someone')
|
||||
|
||||
|
||||
Registering events is a way of saying "this method is an event handler".
|
||||
You can use `telethon.events.is_handler` to check if any method is a handler.
|
||||
You can think of them as a different approach to Flask's blueprints.
|
||||
|
||||
It's important to note that this does **not** add the handler to any client!
|
||||
You never specified the client on which the handler should be used. You only
|
||||
declared that it is a handler, and its type.
|
||||
|
||||
To actually use the handler, you need to `client.add_event_handler
|
||||
<telethon.client.updates.UpdateMethods.add_event_handler>` to the
|
||||
client (or clients) where they should be added to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# main.py
|
||||
from telethon import TelegramClient
|
||||
import handlers.welcome
|
||||
|
||||
with TelegramClient(...) as client:
|
||||
client.add_event_handler(handlers.welcome.handler)
|
||||
client.run_until_disconnected()
|
||||
|
||||
|
||||
This also means that you can register an event handler once and
|
||||
then add it to many clients without re-declaring the event.
|
||||
|
||||
|
||||
Events Without Decorators
|
||||
=========================
|
||||
|
||||
If for any reason you don't want to use `telethon.events.register`,
|
||||
you can explicitly pass the event handler to use to the mentioned
|
||||
`client.add_event_handler
|
||||
<telethon.client.updates.UpdateMethods.add_event_handler>`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, events
|
||||
|
||||
async def handler(event):
|
||||
...
|
||||
|
||||
with TelegramClient(...) as client:
|
||||
client.add_event_handler(handler, events.NewMessage)
|
||||
client.run_until_disconnected()
|
||||
|
||||
|
||||
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`` argument is optional in all three methods and defaults to
|
||||
`events.Raw <telethon.events.raw.Raw>` for adding, and `None` when
|
||||
removing (so all callbacks would be removed).
|
||||
|
||||
.. note::
|
||||
|
||||
The ``event`` type is ignored in `client.add_event_handler
|
||||
<telethon.client.updates.UpdateMethods.add_event_handler>`
|
||||
if you have used `telethon.events.register` on the ``callback``
|
||||
before, since that's the point of using such method at all.
|
||||
|
||||
|
||||
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` if you're looking for
|
||||
the methods reference.
|
||||
|
||||
Understanding asyncio
|
||||
=====================
|
||||
|
||||
|
||||
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
|
||||
|
||||
asyncio.run(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`` for 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())
|
||||
|
||||
Sequential Updates
|
||||
==================
|
||||
|
||||
If you need to process updates sequentially (i.e. not in parallel),
|
||||
you should set ``sequential_updates=True`` when creating the client:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with TelegramClient(..., sequential_updates=True) as client:
|
||||
...
|
|
@ -1,211 +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://tl.telethon.dev'
|
||||
|
||||
# -- 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',
|
||||
'sphinx.ext.autosummary',
|
||||
'sphinx.ext.intersphinx',
|
||||
'custom_roles'
|
||||
]
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/3', None)
|
||||
}
|
||||
|
||||
# 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 - 2019, 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'), 'r') 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 = 'en'
|
||||
|
||||
# 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 = 'friendly'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
def skip(app, what, name, obj, would_skip, options):
|
||||
if name.endswith('__'):
|
||||
# We want to show special methods names, except some which add clutter
|
||||
return name in {
|
||||
'__init__',
|
||||
'__abstractmethods__',
|
||||
'__module__',
|
||||
'__doc__',
|
||||
'__dict__'
|
||||
}
|
||||
|
||||
return would_skip
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.connect("autodoc-skip-member", skip)
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
html_theme_options = {
|
||||
'collapse_navigation': True,
|
||||
'display_version': True,
|
||||
'navigation_depth': 3,
|
||||
}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
# html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# This is required for the alabaster theme
|
||||
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
|
||||
html_sidebars = {
|
||||
'**': [
|
||||
'globaltoc.html',
|
||||
'relations.html', # needs 'show_related': True theme option to display
|
||||
'searchbox.html',
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Telethondoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Telethon.tex', 'Telethon Documentation',
|
||||
author, 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'telethon', 'Telethon Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Telethon', 'Telethon Documentation',
|
||||
author, 'Telethon', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
|
@ -1,67 +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
|
||||
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
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 = {}
|
||||
|
||||
# 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.add_role('tl', tl_role)
|
||||
app.add_config_value('tl_ref_url', None, 'env')
|
||||
return
|
|
@ -1,22 +0,0 @@
|
|||
============
|
||||
Coding Style
|
||||
============
|
||||
|
||||
|
||||
Basically, make it **readable**, while keeping the style similar to the
|
||||
code of whatever file you're working on.
|
||||
|
||||
Also note that not everyone has 4K screens for their primary monitors,
|
||||
so please try to stick to the 80-columns limit. This makes it easy to
|
||||
``git diff`` changes from a terminal before committing changes. If the
|
||||
line has to be long, please don't exceed 120 characters.
|
||||
|
||||
For the commit messages, please make them *explanatory*. Not only
|
||||
they're helpful to troubleshoot when certain issues could have been
|
||||
introduced, but they're also used to construct the change log once a new
|
||||
version is ready.
|
||||
|
||||
If you don't know enough Python, I strongly recommend reading `Dive Into
|
||||
Python 3 <http://www.diveintopython3.net/>`__, available online for
|
||||
free. For instance, remember to do ``if x is None`` or
|
||||
``if x is not None`` instead ``if x == None``!
|
|
@ -1,25 +0,0 @@
|
|||
==========
|
||||
Philosophy
|
||||
==========
|
||||
|
||||
|
||||
The intention of the library is to have an existing MTProto library
|
||||
existing with hardly any dependencies (indeed, wherever Python is
|
||||
available, you can run this library).
|
||||
|
||||
Being written in Python means that performance will be nowhere close to
|
||||
other implementations written in, for instance, Java, C++, Rust, or
|
||||
pretty much any other compiled language. However, the library turns out
|
||||
to actually be pretty decent for common operations such as sending
|
||||
messages, receiving updates, or other scripting. Uploading files may be
|
||||
notably slower, but if you would like to contribute, pull requests are
|
||||
appreciated!
|
||||
|
||||
If ``libssl`` is available on your system, the library will make use of
|
||||
it to speed up some critical parts such as encrypting and decrypting the
|
||||
messages. Files will notably be sent and downloaded faster.
|
||||
|
||||
The main focus is to keep everything clean and simple, for everyone to
|
||||
understand how working with MTProto and Telegram works. Don't be afraid
|
||||
to read the source, the code won't bite you! It may prove useful when
|
||||
using the library on your own use cases.
|
|
@ -1,51 +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).
|
||||
|
||||
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.
|
||||
|
||||
If you clone the repository, you will have to run ``python setup.py gen``
|
||||
in order to generate the code. Installing the library runs the generator
|
||||
too, but the mentioned command will just generate code.
|
|
@ -1,13 +0,0 @@
|
|||
===============================
|
||||
Telegram API in Other Languages
|
||||
===============================
|
||||
|
||||
Telethon was made for **Python**, and it has inspired other libraries such as
|
||||
`gramjs <https://github.com/gram-js/gramjs>`__ (JavaScript) and `grammers
|
||||
<https://github.com/Lonami/grammers>`__ (Rust). But there is a lot more beyond
|
||||
those, made independently by different developers.
|
||||
|
||||
If you're looking for something like Telethon but in a different programming
|
||||
language, head over to `Telegram API in Other Languages in the official wiki
|
||||
<https://github.com/LonamiWebs/Telethon/wiki/Telegram-API-in-Other-Languages>`__
|
||||
for a (mostly) up-to-date list.
|
|
@ -1,41 +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)
|
||||
client.start(
|
||||
phone='9996621234', code_callback=lambda: '22222'
|
||||
)
|
||||
|
||||
Note that Telegram has changed the length of login codes multiple times in the
|
||||
past, so if ``dc_id`` repeated five times does not work, try repeating it six
|
||||
times.
|
|
@ -1,87 +0,0 @@
|
|||
=====
|
||||
Tests
|
||||
=====
|
||||
|
||||
Telethon uses `Pytest <https://pytest.org/>`__, for testing, `Tox
|
||||
<https://tox.readthedocs.io/en/latest/>`__ for environment setup, and
|
||||
`pytest-asyncio <https://pypi.org/project/pytest-asyncio/>`__ and `pytest-cov
|
||||
<https://pytest-cov.readthedocs.io/en/latest/>`__ for asyncio and
|
||||
`coverage <https://coverage.readthedocs.io/>`__ integration.
|
||||
|
||||
While reading the full documentation for these is probably a good idea, there
|
||||
is a lot to read, so a brief summary of these tools is provided below for
|
||||
convienience.
|
||||
|
||||
Brief Introduction to Pytest
|
||||
============================
|
||||
|
||||
`Pytest <https://pytest.org/>`__ is a tool for discovering and running python
|
||||
tests, as well as allowing modular reuse of test setup code using fixtures.
|
||||
|
||||
Most Pytest tests will look something like this::
|
||||
|
||||
from module import my_thing, my_other_thing
|
||||
|
||||
def test_my_thing(fixture):
|
||||
assert my_thing(fixture) == 42
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_my_thing(event_loop):
|
||||
assert await my_other_thing(loop=event_loop) == 42
|
||||
|
||||
Note here:
|
||||
|
||||
1. The test imports one specific function. The role of unit tests is to test
|
||||
that the implementation of some unit, like a function or class, works.
|
||||
It's role is not so much to test that components interact well with each
|
||||
other. I/O, such as connecting to remote servers, should be avoided. This
|
||||
helps with quickly identifying the source of an error, finding silent
|
||||
breakage, and makes it easier to cover all possible code paths.
|
||||
|
||||
System or integration tests can also be useful, but are currently out of
|
||||
scope of Telethon's automated testing.
|
||||
|
||||
2. A function ``test_my_thing`` is declared. Pytest searches for files
|
||||
starting with ``test_``, classes starting with ``Test`` and executes any
|
||||
functions or methods starting with ``test_`` it finds.
|
||||
|
||||
3. The function is declared with a parameter ``fixture``. Fixtures are used to
|
||||
request things required to run the test, such as temporary directories,
|
||||
free TCP ports, Connections, etc. Fixtures are declared by simply adding
|
||||
the fixture name as parameter. A full list of available fixtures can be
|
||||
found with the ``pytest --fixtures`` command.
|
||||
|
||||
4. The test uses a simple ``assert`` to test some condition is valid. Pytest
|
||||
uses some magic to ensure that the errors from this are readable and easy
|
||||
to debug.
|
||||
|
||||
5. The ``pytest.mark.asyncio`` fixture is provided by ``pytest-asyncio``. It
|
||||
starts a loop and executes a test function as coroutine. This should be
|
||||
used for testing asyncio code. It also declares the ``event_loop``
|
||||
fixture, which will request an ``asyncio`` event loop.
|
||||
|
||||
Brief Introduction to Tox
|
||||
=========================
|
||||
|
||||
`Tox <https://tox.readthedocs.io/en/latest/>`__ is a tool for automated setup
|
||||
of virtual environments for testing. While the tests can be run directly by
|
||||
just running ``pytest``, this only tests one specific python version in your
|
||||
existing environment, which will not catch e.g. undeclared dependencies, or
|
||||
version incompatabilities.
|
||||
|
||||
Tox environments are declared in the ``tox.ini`` file. The default
|
||||
environments, declared at the top, can be simply run with ``tox``. The option
|
||||
``tox -e py36,flake`` can be used to request specific environments to be run.
|
||||
|
||||
Brief Introduction to Pytest-cov
|
||||
================================
|
||||
|
||||
Coverage is a useful metric for testing. It measures the lines of code and
|
||||
branches that are exercised by the tests. The higher the coverage, the more
|
||||
likely it is that any coding errors will be caught by the tests.
|
||||
|
||||
A brief coverage report can be generated with the ``--cov`` option to ``tox``,
|
||||
which will be passed on to ``pytest``. Additionally, the very useful HTML
|
||||
report can be generated with ``--cov --cov-report=html``, which contains a
|
||||
browsable copy of the source code, annotated with coverage information for each
|
||||
line.
|
|
@ -1,17 +0,0 @@
|
|||
============================
|
||||
Tips for Porting the Project
|
||||
============================
|
||||
|
||||
|
||||
If you're going to use the code on this repository to guide you, please
|
||||
be kind and don't forget to mention it helped you!
|
||||
|
||||
You should start by reading the source code on the `first
|
||||
release <https://github.com/LonamiWebs/Telethon/releases/tag/v0.1>`__ of
|
||||
the project, and start creating a ``MTProtoSender``. Once this is made,
|
||||
you should write by hand the code to authenticate on the Telegram's
|
||||
server, which are some steps required to get the key required to talk to
|
||||
them. Save it somewhere! Then, simply mimic, or reinvent other parts of
|
||||
the code, and it will be ready to go within a few days.
|
||||
|
||||
Good luck!
|
|
@ -1,33 +0,0 @@
|
|||
===============================
|
||||
Understanding the Type Language
|
||||
===============================
|
||||
|
||||
|
||||
`Telegram's Type Language <https://core.telegram.org/mtproto/TL>`__
|
||||
(also known as TL, found on ``.tl`` files) is a concise way to define
|
||||
what other programming languages commonly call classes or structs.
|
||||
|
||||
Every definition is written as follows for a Telegram object is defined
|
||||
as follows:
|
||||
|
||||
``name#id argument_name:argument_type = CommonType``
|
||||
|
||||
This means that in a single line you know what the ``TLObject`` name is.
|
||||
You know it's unique ID, and you know what arguments it has. It really
|
||||
isn't that hard to write a generator for generating code to any
|
||||
platform!
|
||||
|
||||
The generated code should also be able to *encode* the ``TLObject`` (let
|
||||
this be a request or a type) into bytes, so they can be sent over the
|
||||
network. This isn't a big deal either, because you know how the
|
||||
``TLObject``\ 's are made, and how the types should be serialized.
|
||||
|
||||
You can either write your own code generator, or use the one this
|
||||
library provides, but please be kind and keep some special mention to
|
||||
this project for helping you out.
|
||||
|
||||
This is only a introduction. The ``TL`` language is not *that* easy. But
|
||||
it's not that hard either. You're free to sniff the
|
||||
``telethon_generator/`` files and learn how to parse other more complex
|
||||
lines, such as ``flags`` (to indicate things that may or may not be
|
||||
written at all) and ``vector``\ 's.
|
|
@ -1,128 +0,0 @@
|
|||
===============================
|
||||
Working with Chats and Channels
|
||||
===============================
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
These examples assume you have read :ref:`full-api`.
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
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
|
||||
await client(JoinChannelRequest(channel))
|
||||
|
||||
# In the same way, you can also leave such channel
|
||||
from telethon.tl.functions.channels import LeaveChannelRequest
|
||||
await client(LeaveChannelRequest(input_channel))
|
||||
|
||||
|
||||
For more on channels, check the `channels namespace`__.
|
||||
|
||||
|
||||
__ https://tl.telethon.dev/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 = await 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``).
|
||||
await 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
|
||||
|
||||
await client(InviteToChannelRequest(
|
||||
channel,
|
||||
[users_to_add]
|
||||
))
|
||||
|
||||
Note that this method will only really work for friends or bot accounts.
|
||||
Trying to mass-add users with this approach will not work, and can put both
|
||||
your account and group to risk, possibly being flagged as spam and limited.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
await client(GetMessagesViewsRequest(
|
||||
peer=channel,
|
||||
id=msg_ids,
|
||||
increment=True
|
||||
))
|
||||
|
||||
|
||||
Note that you can only do this **once or twice a day** per account,
|
||||
running this in a loop will obviously not increase the views forever
|
||||
unless you wait a day between each iteration. If you run it any sooner
|
||||
than that, the views simply won't be increased.
|
||||
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/233
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/305
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/409
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/447
|
|
@ -1,74 +0,0 @@
|
|||
=====
|
||||
Users
|
||||
=====
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
These examples assume you have read :ref:`full-api`.
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
Retrieving full information
|
||||
===========================
|
||||
|
||||
If you need to retrieve the bio, biography or about information for a user
|
||||
you should use :tl:`GetFullUser`:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.users import GetFullUserRequest
|
||||
|
||||
full = await client(GetFullUserRequest(user))
|
||||
# or even
|
||||
full = await client(GetFullUserRequest('username'))
|
||||
|
||||
bio = full.full_user.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
|
||||
|
||||
await client(UpdateProfileRequest(
|
||||
about='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
|
||||
|
||||
await 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
|
||||
|
||||
await client(UploadProfilePhotoRequest(
|
||||
await client.upload_file('/path/to/some/file')
|
||||
))
|
|
@ -1,17 +0,0 @@
|
|||
=================
|
||||
A Word of Warning
|
||||
=================
|
||||
|
||||
Full API is **not** how you are intended to use the library. You **should**
|
||||
always prefer the :ref:`client-ref`. However, not everything is implemented
|
||||
as a friendly method, so full API is your last resort.
|
||||
|
||||
If you select a method in :ref:`client-ref`, you will most likely find an
|
||||
example for that method. This is how you are intended to use the library.
|
||||
|
||||
Full API **will** break between different minor versions of the library,
|
||||
since Telegram changes very often. The friendly methods will be kept
|
||||
compatible between major versions.
|
||||
|
||||
If you need to see real-world examples, please refer to the
|
||||
`wiki page of projects using Telethon <https://github.com/LonamiWebs/Telethon/wiki/Projects-using-Telethon>`__.
|
|
@ -1,13 +0,0 @@
|
|||
=====================
|
||||
Working with messages
|
||||
=====================
|
||||
|
||||
.. note::
|
||||
|
||||
These examples assume you have read :ref:`full-api`.
|
||||
|
||||
This section has been `moved to the wiki`_, where it can be easily edited as new
|
||||
features arrive and the API changes. Please refer to the linked page to learn how
|
||||
to send spoilers, custom emoji, stickers, react to messages, and more things.
|
||||
|
||||
.. _moved to the wiki: https://github.com/LonamiWebs/Telethon/wiki/Sending-more-than-just-messages
|
|
@ -1,120 +0,0 @@
|
|||
========================
|
||||
Telethon's Documentation
|
||||
========================
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.sync import TelegramClient, events
|
||||
|
||||
with TelegramClient('name', api_id, api_hash) as client:
|
||||
client.send_message('me', 'Hello, myself!')
|
||||
print(client.download_profile_photo('me'))
|
||||
|
||||
@client.on(events.NewMessage(pattern='(?i).*Hello'))
|
||||
async def handler(event):
|
||||
await event.reply('Hey!')
|
||||
|
||||
client.run_until_disconnected()
|
||||
|
||||
|
||||
* Are you new here? Jump straight into :ref:`installation`!
|
||||
* Looking for the method reference? See :ref:`client-ref`.
|
||||
* Did you upgrade the library? Please read :ref:`changelog`.
|
||||
* Used Telethon before v1.0? See :ref:`compatibility-and-convenience`.
|
||||
* Coming from Bot API or want to create new bots? See :ref:`botapi`.
|
||||
* Need the full API reference? https://tl.telethon.dev/.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
How should I use the documentation?
|
||||
-----------------------------------
|
||||
|
||||
If you are getting started with the library, you should follow the
|
||||
documentation in order by pressing the "Next" button at the bottom-right
|
||||
of every page.
|
||||
|
||||
You can also use the menu on the left to quickly skip over sections.
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
:caption: First Steps
|
||||
|
||||
basic/installation
|
||||
basic/signing-in
|
||||
basic/quick-start
|
||||
basic/updates
|
||||
basic/next-steps
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
:caption: Quick References
|
||||
|
||||
quick-references/faq
|
||||
quick-references/client-reference
|
||||
quick-references/events-reference
|
||||
quick-references/objects-reference
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
:caption: Concepts
|
||||
|
||||
concepts/strings
|
||||
concepts/entities
|
||||
concepts/chats-vs-channels
|
||||
concepts/updates
|
||||
concepts/sessions
|
||||
concepts/full-api
|
||||
concepts/errors
|
||||
concepts/botapi-vs-mtproto
|
||||
concepts/asyncio
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
:caption: Full API Examples
|
||||
|
||||
examples/word-of-warning
|
||||
examples/chats-and-channels
|
||||
examples/users
|
||||
examples/working-with-messages
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
:caption: Developing
|
||||
|
||||
developing/philosophy.rst
|
||||
developing/test-servers.rst
|
||||
developing/project-structure.rst
|
||||
developing/coding-style.rst
|
||||
developing/testing.rst
|
||||
developing/understanding-the-type-language.rst
|
||||
developing/tips-for-porting-the-project.rst
|
||||
developing/telegram-api-in-other-languages.rst
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
:caption: Miscellaneous
|
||||
|
||||
misc/changelog
|
||||
misc/wall-of-shame.rst
|
||||
misc/compatibility-and-convenience
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
:caption: Telethon Modules
|
||||
|
||||
modules/client
|
||||
modules/events
|
||||
modules/custom
|
||||
modules/utils
|
||||
modules/errors
|
||||
modules/sessions
|
||||
modules/network
|
||||
modules/helpers
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -1,185 +0,0 @@
|
|||
.. _compatibility-and-convenience:
|
||||
|
||||
=============================
|
||||
Compatibility and Convenience
|
||||
=============================
|
||||
|
||||
Telethon is an `asyncio` library. Compatibility is an important concern,
|
||||
and while it can't always be kept and mistakes happens, the :ref:`changelog`
|
||||
is there to tell you when these important changes happen.
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
Compatibility
|
||||
=============
|
||||
|
||||
Some decisions when developing will inevitable be proven wrong in the future.
|
||||
One of these decisions was using threads. Now that Python 3.4 is reaching EOL
|
||||
and using `asyncio` is usable as of Python 3.5 it makes sense for a library
|
||||
like Telethon to make a good use of it.
|
||||
|
||||
If you have old code, **just use old versions** of the library! There is
|
||||
nothing wrong with that other than not getting new updates or fixes, but
|
||||
using a fixed version with ``pip install telethon==0.19.1.6`` is easy
|
||||
enough to do.
|
||||
|
||||
You might want to consider using `Virtual Environments
|
||||
<https://docs.python.org/3/tutorial/venv.html>`_ in your projects.
|
||||
|
||||
There's no point in maintaining a synchronous version because the whole point
|
||||
is that people don't have time to upgrade, and there has been several changes
|
||||
and clean-ups. Using an older version is the right way to go.
|
||||
|
||||
Sometimes, other small decisions are made. These all will be reflected in the
|
||||
:ref:`changelog` which you should read when upgrading.
|
||||
|
||||
If you want to jump the `asyncio` boat, here are some of the things you will
|
||||
need to start migrating really old code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# 1. Import the client from telethon.sync
|
||||
from telethon.sync import TelegramClient
|
||||
|
||||
# 2. Change this monster...
|
||||
try:
|
||||
assert client.connect()
|
||||
if not client.is_user_authorized():
|
||||
client.send_code_request(phone_number)
|
||||
me = client.sign_in(phone_number, input('Enter code: '))
|
||||
|
||||
... # REST OF YOUR CODE
|
||||
finally:
|
||||
client.disconnect()
|
||||
|
||||
# ...for this:
|
||||
with client:
|
||||
... # REST OF YOUR CODE
|
||||
|
||||
# 3. client.idle() no longer exists.
|
||||
# Change this...
|
||||
client.idle()
|
||||
# ...to this:
|
||||
client.run_until_disconnected()
|
||||
|
||||
# 4. client.add_update_handler no longer exists.
|
||||
# Change this...
|
||||
client.add_update_handler(handler)
|
||||
# ...to this:
|
||||
client.add_event_handler(handler)
|
||||
|
||||
|
||||
In addition, all the update handlers must be ``async def``, and you need
|
||||
to ``await`` method calls that rely on network requests, such as getting
|
||||
the chat or sender. If you don't use updates, you're done!
|
||||
|
||||
|
||||
Convenience
|
||||
===========
|
||||
|
||||
.. note::
|
||||
|
||||
The entire documentation assumes you have done one of the following:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, sync
|
||||
# or
|
||||
from telethon.sync import TelegramClient
|
||||
|
||||
This makes the examples shorter and easier to think about.
|
||||
|
||||
For quick scripts that don't need updates, it's a lot more convenient to
|
||||
forget about `asyncio` and just work with sequential code. This can prove
|
||||
to be a powerful hybrid for running under the Python REPL too.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.sync import TelegramClient
|
||||
# ^~~~~ note this part; it will manage the asyncio loop for you
|
||||
|
||||
with TelegramClient(...) as client:
|
||||
print(client.get_me().username)
|
||||
# ^ notice the lack of await, or loop.run_until_complete().
|
||||
# Since there is no loop running, this is done behind the scenes.
|
||||
#
|
||||
message = client.send_message('me', 'Hi!')
|
||||
import time
|
||||
time.sleep(5)
|
||||
message.delete()
|
||||
|
||||
# You can also have an hybrid between a synchronous
|
||||
# part and asynchronous event handlers.
|
||||
#
|
||||
from telethon import events
|
||||
@client.on(events.NewMessage(pattern='(?i)hi|hello'))
|
||||
async def handler(event):
|
||||
await event.reply('hey')
|
||||
|
||||
client.run_until_disconnected()
|
||||
|
||||
|
||||
Some methods, such as ``with``, ``start``, ``disconnect`` and
|
||||
``run_until_disconnected`` work both in synchronous and asynchronous
|
||||
contexts by default for convenience, and to avoid the little overhead
|
||||
it has when using methods like sending a message, getting messages, etc.
|
||||
This keeps the best of both worlds as a sane default.
|
||||
|
||||
.. note::
|
||||
|
||||
As a rule of thumb, if you're inside an ``async def`` and you need
|
||||
the client, you need to ``await`` calls to the API. If you call other
|
||||
functions that also need API calls, make them ``async def`` and ``await``
|
||||
them too. Otherwise, there is no need to do so with this mode.
|
||||
|
||||
Speed
|
||||
=====
|
||||
|
||||
When you're ready to micro-optimize your application, or if you simply
|
||||
don't need to call any non-basic methods from a synchronous context,
|
||||
just get rid of ``telethon.sync`` and work inside an ``async def``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
from telethon import TelegramClient, events
|
||||
|
||||
async def main():
|
||||
async with TelegramClient(...) as client:
|
||||
print((await client.get_me()).username)
|
||||
# ^_____________________^ notice these parenthesis
|
||||
# You want to ``await`` the call, not the username.
|
||||
#
|
||||
message = await client.send_message('me', 'Hi!')
|
||||
await asyncio.sleep(5)
|
||||
await message.delete()
|
||||
|
||||
@client.on(events.NewMessage(pattern='(?i)hi|hello'))
|
||||
async def handler(event):
|
||||
await event.reply('hey')
|
||||
|
||||
await client.run_until_disconnected()
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
The ``telethon.sync`` magic module essentially wraps every method behind:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
With some other tricks, so that you don't have to write it yourself every time.
|
||||
That's the overhead you pay if you import it, and what you save if you don't.
|
||||
|
||||
Learning
|
||||
========
|
||||
|
||||
You know the library uses `asyncio` everywhere, and you want to learn
|
||||
how to do things right. Even though `asyncio` is its own topic, the
|
||||
documentation wants you to learn how to use Telethon correctly, and for
|
||||
that, you need to use `asyncio` correctly too. For this reason, there
|
||||
is a section called :ref:`mastering-asyncio` that will introduce you to
|
||||
the `asyncio` world, with links to more resources for learning how to
|
||||
use it. Feel free to check that section out once you have read the rest.
|
|
@ -1,65 +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://tl.telethon.dev/>`__,
|
||||
you will end up on the `Wall of
|
||||
Shame <https://github.com/LonamiWebs/Telethon/issues?q=is%3Aissue+label%3ARTFM+is%3Aclosed>`__,
|
||||
i.e. all issues labeled
|
||||
`"RTFM" <http://www.urbandictionary.com/define.php?term=RTFM>`__:
|
||||
|
||||
**rtfm**
|
||||
Literally "Read The F--king Manual"; a term showing the
|
||||
frustration of being bothered with questions so trivial that the asker
|
||||
could have quickly figured out the answer on their own with minimal
|
||||
effort, usually by reading readily-available documents. People who
|
||||
say"RTFM!" might be considered rude, but the true rude ones are the
|
||||
annoying people who take absolutely no self-responibility and expect to
|
||||
have all the answers handed to them personally.
|
||||
|
||||
*"Damn, that's the twelveth time that somebody posted this question
|
||||
to the messageboard today! RTFM, already!"*
|
||||
|
||||
*by Bill M. July 27, 2004*
|
||||
|
||||
If you have indeed read the docs, and have tried looking for the method,
|
||||
and yet you didn't find what you need, **that's fine**. Telegram's API
|
||||
can have some obscure names at times, and for this reason, there is a
|
||||
`"question"
|
||||
label <https://github.com/LonamiWebs/Telethon/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20label%3Aquestion%20>`__
|
||||
with questions that are okay to ask. Just state what you've tried so
|
||||
that we know you've made an effort, or you'll go to the Wall of Shame.
|
||||
|
||||
Of course, if the issue you're going to open is not even a question but
|
||||
a real issue with the library (thankfully, most of the issues have been
|
||||
that!), you won't end up here. Don't worry.
|
||||
|
||||
Current winner
|
||||
--------------
|
||||
|
||||
The current winner is `issue
|
||||
213 <https://github.com/LonamiWebs/Telethon/issues/213>`__:
|
||||
|
||||
**Issue:**
|
||||
|
||||
.. figure:: https://user-images.githubusercontent.com/6297805/29822978-9a9a6ef0-8ccd-11e7-9ec5-934ea0f57681.jpg
|
||||
|
||||
:alt: Winner issue
|
||||
|
||||
Winner issue
|
||||
|
||||
**Answer:**
|
||||
|
||||
.. figure:: https://user-images.githubusercontent.com/6297805/29822983-9d523402-8ccd-11e7-9fb1-5783740ee366.jpg
|
||||
|
||||
:alt: Winner issue answer
|
||||
|
||||
Winner issue answer
|
|
@ -1,103 +0,0 @@
|
|||
.. _telethon-client:
|
||||
|
||||
==============
|
||||
TelegramClient
|
||||
==============
|
||||
|
||||
.. currentmodule:: telethon.client
|
||||
|
||||
The `TelegramClient <telegramclient.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
|
||||
|
||||
from telethon import TelegramClient
|
||||
|
||||
client = TelegramClient(name, api_id, api_hash)
|
||||
|
||||
async def main():
|
||||
# Now you can use all client methods listed below, like for example...
|
||||
await client.send_message('me', 'Hello to myself!')
|
||||
|
||||
with client:
|
||||
client.loop.run_until_complete(main())
|
||||
|
||||
|
||||
You **don't** need to import these `AuthMethods`, `MessageMethods`, etc.
|
||||
Together they are the `TelegramClient <telegramclient.TelegramClient>` and
|
||||
you can access all of their methods.
|
||||
|
||||
See :ref:`client-ref` for a short summary.
|
||||
|
||||
.. automodule:: telethon.client.telegramclient
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.client.telegrambaseclient
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.client.account
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.client.auth
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.client.bots
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.client.buttons
|
||||
: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.downloads
|
||||
: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.uploads
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.client.users
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
|
@ -1,163 +0,0 @@
|
|||
==============
|
||||
Custom package
|
||||
==============
|
||||
|
||||
The `telethon.tl.custom` package contains custom classes that the library
|
||||
uses in order to make working with Telegram easier. Only those that you
|
||||
are supposed to use will be documented here. You can use undocumented ones
|
||||
at your own risk.
|
||||
|
||||
More often than not, you don't need to import these (unless you want
|
||||
type hinting), nor do you need to manually create instances of these
|
||||
classes. They are returned by client methods.
|
||||
|
||||
.. contents::
|
||||
|
||||
.. automodule:: telethon.tl.custom
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
AdminLogEvent
|
||||
=============
|
||||
|
||||
.. automodule:: telethon.tl.custom.adminlogevent
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Button
|
||||
======
|
||||
|
||||
.. automodule:: telethon.tl.custom.button
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
ChatGetter
|
||||
==========
|
||||
|
||||
.. automodule:: telethon.tl.custom.chatgetter
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Conversation
|
||||
============
|
||||
|
||||
.. automodule:: telethon.tl.custom.conversation
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Dialog
|
||||
======
|
||||
|
||||
.. automodule:: telethon.tl.custom.dialog
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Draft
|
||||
=====
|
||||
|
||||
.. automodule:: telethon.tl.custom.draft
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
File
|
||||
====
|
||||
|
||||
.. automodule:: telethon.tl.custom.file
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Forward
|
||||
=======
|
||||
|
||||
.. automodule:: telethon.tl.custom.forward
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
InlineBuilder
|
||||
=============
|
||||
|
||||
.. automodule:: telethon.tl.custom.inlinebuilder
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
InlineResult
|
||||
============
|
||||
|
||||
.. automodule:: telethon.tl.custom.inlineresult
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
InlineResults
|
||||
=============
|
||||
|
||||
.. automodule:: telethon.tl.custom.inlineresults
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Message
|
||||
=======
|
||||
|
||||
.. automodule:: telethon.tl.custom.message
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
MessageButton
|
||||
=============
|
||||
|
||||
.. automodule:: telethon.tl.custom.messagebutton
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
ParticipantPermissions
|
||||
======================
|
||||
|
||||
.. automodule:: telethon.tl.custom.participantpermissions
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
QRLogin
|
||||
=======
|
||||
|
||||
.. automodule:: telethon.tl.custom.qrlogin
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
SenderGetter
|
||||
============
|
||||
|
||||
.. automodule:: telethon.tl.custom.sendergetter
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
|
@ -1,20 +0,0 @@
|
|||
.. _telethon-errors:
|
||||
|
||||
==========
|
||||
API Errors
|
||||
==========
|
||||
|
||||
These are the base errors that Telegram's API may raise.
|
||||
|
||||
See :ref:`rpc-errors` for a more in-depth explanation on how to handle all
|
||||
known possible errors and learning to determine what a method may raise.
|
||||
|
||||
.. automodule:: telethon.errors.common
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.errors.rpcbaseerrors
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
|
@ -1,70 +0,0 @@
|
|||
.. _telethon-events:
|
||||
|
||||
=============
|
||||
Update Events
|
||||
=============
|
||||
|
||||
.. currentmodule:: telethon.events
|
||||
|
||||
Every event (builder) subclasses `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.callbackquery
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.events.inlinequery
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.events.album
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.events.raw
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.events
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
|
@ -1,8 +0,0 @@
|
|||
=======
|
||||
Helpers
|
||||
=======
|
||||
|
||||
.. automodule:: telethon.helpers
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
|
@ -1,33 +0,0 @@
|
|||
.. _telethon-network:
|
||||
|
||||
================
|
||||
Connection Modes
|
||||
================
|
||||
|
||||
The only part about network that you should worry about are
|
||||
the different connection modes, which are the following:
|
||||
|
||||
.. automodule:: telethon.network.connection.tcpfull
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.network.connection.tcpabridged
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.network.connection.tcpintermediate
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.network.connection.tcpobfuscated
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.network.connection.http
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
|
@ -1,27 +0,0 @@
|
|||
.. _telethon-sessions:
|
||||
|
||||
========
|
||||
Sessions
|
||||
========
|
||||
|
||||
These are the different built-in session storage that you may subclass.
|
||||
|
||||
.. automodule:: telethon.sessions.abstract
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.sessions.memory
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.sessions.sqlite
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: telethon.sessions.string
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
|
@ -1,12 +0,0 @@
|
|||
.. _telethon-utils:
|
||||
|
||||
=========
|
||||
Utilities
|
||||
=========
|
||||
|
||||
These are the utilities that the library has to offer.
|
||||
|
||||
.. automodule:: telethon.utils
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
|
@ -1,202 +0,0 @@
|
|||
.. _client-ref:
|
||||
|
||||
================
|
||||
Client Reference
|
||||
================
|
||||
|
||||
This page contains a summary of all the important methods and properties that
|
||||
you may need when using Telethon. They are sorted by relevance and are not in
|
||||
alphabetical order.
|
||||
|
||||
You should use this page to learn about which methods are available, and
|
||||
if you need a usage example or further description of the arguments, be
|
||||
sure to follow the links.
|
||||
|
||||
.. contents::
|
||||
|
||||
TelegramClient
|
||||
==============
|
||||
|
||||
This is a summary of the methods and
|
||||
properties you will find at :ref:`telethon-client`.
|
||||
|
||||
Auth
|
||||
----
|
||||
|
||||
.. currentmodule:: telethon.client.auth.AuthMethods
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
start
|
||||
send_code_request
|
||||
sign_in
|
||||
qr_login
|
||||
log_out
|
||||
edit_2fa
|
||||
|
||||
Base
|
||||
----
|
||||
|
||||
.. py:currentmodule:: telethon.client.telegrambaseclient.TelegramBaseClient
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
connect
|
||||
disconnect
|
||||
is_connected
|
||||
disconnected
|
||||
loop
|
||||
set_proxy
|
||||
|
||||
Messages
|
||||
--------
|
||||
|
||||
.. py:currentmodule:: telethon.client.messages.MessageMethods
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
send_message
|
||||
edit_message
|
||||
delete_messages
|
||||
forward_messages
|
||||
iter_messages
|
||||
get_messages
|
||||
pin_message
|
||||
unpin_message
|
||||
send_read_acknowledge
|
||||
|
||||
Uploads
|
||||
-------
|
||||
|
||||
.. py:currentmodule:: telethon.client.uploads.UploadMethods
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
send_file
|
||||
upload_file
|
||||
|
||||
Downloads
|
||||
---------
|
||||
|
||||
.. currentmodule:: telethon.client.downloads.DownloadMethods
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
download_media
|
||||
download_profile_photo
|
||||
download_file
|
||||
iter_download
|
||||
|
||||
Dialogs
|
||||
-------
|
||||
|
||||
.. py:currentmodule:: telethon.client.dialogs.DialogMethods
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
iter_dialogs
|
||||
get_dialogs
|
||||
edit_folder
|
||||
iter_drafts
|
||||
get_drafts
|
||||
delete_dialog
|
||||
conversation
|
||||
|
||||
Users
|
||||
-----
|
||||
|
||||
.. py:currentmodule:: telethon.client.users.UserMethods
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
get_me
|
||||
is_bot
|
||||
is_user_authorized
|
||||
get_entity
|
||||
get_input_entity
|
||||
get_peer_id
|
||||
|
||||
Chats
|
||||
-----
|
||||
|
||||
.. currentmodule:: telethon.client.chats.ChatMethods
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
iter_participants
|
||||
get_participants
|
||||
kick_participant
|
||||
iter_admin_log
|
||||
get_admin_log
|
||||
iter_profile_photos
|
||||
get_profile_photos
|
||||
edit_admin
|
||||
edit_permissions
|
||||
get_permissions
|
||||
get_stats
|
||||
action
|
||||
|
||||
Parse Mode
|
||||
----------
|
||||
|
||||
.. py:currentmodule:: telethon.client.messageparse.MessageParseMethods
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
parse_mode
|
||||
|
||||
Updates
|
||||
-------
|
||||
|
||||
.. py:currentmodule:: telethon.client.updates.UpdateMethods
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
on
|
||||
run_until_disconnected
|
||||
add_event_handler
|
||||
remove_event_handler
|
||||
list_event_handlers
|
||||
catch_up
|
||||
set_receive_updates
|
||||
|
||||
Bots
|
||||
----
|
||||
|
||||
.. currentmodule:: telethon.client.bots.BotMethods
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
inline_query
|
||||
|
||||
Buttons
|
||||
-------
|
||||
|
||||
.. currentmodule:: telethon.client.buttons.ButtonMethods
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
build_reply_markup
|
||||
|
||||
Account
|
||||
-------
|
||||
|
||||
.. currentmodule:: telethon.client.account.AccountMethods
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
takeout
|
||||
end_takeout
|
|
@ -1,247 +0,0 @@
|
|||
================
|
||||
Events Reference
|
||||
================
|
||||
|
||||
Here you will find a quick summary of all the methods
|
||||
and properties that you can access when working with events.
|
||||
|
||||
You can access the client that creates this event by doing
|
||||
``event.client``, and you should view the description of the
|
||||
events to find out what arguments it allows on creation and
|
||||
its **attributes** (the properties will be shown here).
|
||||
|
||||
.. important::
|
||||
|
||||
Remember that **all events base** `ChatGetter
|
||||
<telethon.tl.custom.chatgetter.ChatGetter>`! Please see :ref:`faq`
|
||||
if you don't know what this means or the implications of it.
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
NewMessage
|
||||
==========
|
||||
|
||||
Occurs whenever a new text message or a message with media arrives.
|
||||
|
||||
.. note::
|
||||
|
||||
The new message event **should be treated as** a
|
||||
normal `Message <telethon.tl.custom.message.Message>`, with
|
||||
the following exceptions:
|
||||
|
||||
* ``pattern_match`` is the match object returned by ``pattern=``.
|
||||
* ``message`` is **not** the message string. It's the `Message
|
||||
<telethon.tl.custom.message.Message>` object.
|
||||
|
||||
Remember, this event is just a proxy over the message, so while
|
||||
you won't see its attributes and properties, you can still access
|
||||
them. Please see the full documentation for examples.
|
||||
|
||||
Full documentation for the `NewMessage
|
||||
<telethon.events.newmessage.NewMessage>`.
|
||||
|
||||
|
||||
MessageEdited
|
||||
=============
|
||||
|
||||
Occurs whenever a message is edited. Just like `NewMessage
|
||||
<telethon.events.newmessage.NewMessage>`, you should treat
|
||||
this event as a `Message <telethon.tl.custom.message.Message>`.
|
||||
|
||||
Full documentation for the `MessageEdited
|
||||
<telethon.events.messageedited.MessageEdited>`.
|
||||
|
||||
|
||||
MessageDeleted
|
||||
==============
|
||||
|
||||
Occurs whenever a message is deleted. Note that this event isn't 100%
|
||||
reliable, since Telegram doesn't always notify the clients that a message
|
||||
was deleted.
|
||||
|
||||
It only has the ``deleted_id`` and ``deleted_ids`` attributes
|
||||
(in addition to the chat if the deletion happened in a channel).
|
||||
|
||||
Full documentation for the `MessageDeleted
|
||||
<telethon.events.messagedeleted.MessageDeleted>`.
|
||||
|
||||
|
||||
MessageRead
|
||||
===========
|
||||
|
||||
Occurs whenever one or more messages are read in a chat.
|
||||
|
||||
Full documentation for the `MessageRead
|
||||
<telethon.events.messageread.MessageRead>`.
|
||||
|
||||
.. currentmodule:: telethon.events.messageread.MessageRead.Event
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
inbox
|
||||
message_ids
|
||||
|
||||
get_messages
|
||||
is_read
|
||||
|
||||
|
||||
ChatAction
|
||||
==========
|
||||
|
||||
Occurs on certain chat actions, such as chat title changes,
|
||||
user join or leaves, pinned messages, photo changes, etc.
|
||||
|
||||
Full documentation for the `ChatAction
|
||||
<telethon.events.chataction.ChatAction>`.
|
||||
|
||||
.. currentmodule:: telethon.events.chataction.ChatAction.Event
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
added_by
|
||||
kicked_by
|
||||
user
|
||||
input_user
|
||||
user_id
|
||||
users
|
||||
input_users
|
||||
user_ids
|
||||
|
||||
respond
|
||||
reply
|
||||
delete
|
||||
get_pinned_message
|
||||
get_added_by
|
||||
get_kicked_by
|
||||
get_user
|
||||
get_input_user
|
||||
get_users
|
||||
get_input_users
|
||||
|
||||
|
||||
UserUpdate
|
||||
==========
|
||||
|
||||
Occurs whenever a user goes online, starts typing, etc.
|
||||
|
||||
Full documentation for the `UserUpdate
|
||||
<telethon.events.userupdate.UserUpdate>`.
|
||||
|
||||
.. currentmodule:: telethon.events.userupdate.UserUpdate.Event
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
user
|
||||
input_user
|
||||
user_id
|
||||
|
||||
get_user
|
||||
get_input_user
|
||||
|
||||
typing
|
||||
uploading
|
||||
recording
|
||||
playing
|
||||
cancel
|
||||
geo
|
||||
audio
|
||||
round
|
||||
video
|
||||
contact
|
||||
document
|
||||
photo
|
||||
last_seen
|
||||
until
|
||||
online
|
||||
recently
|
||||
within_weeks
|
||||
within_months
|
||||
|
||||
|
||||
CallbackQuery
|
||||
=============
|
||||
|
||||
Occurs whenever you sign in as a bot and a user
|
||||
clicks one of the inline buttons on your messages.
|
||||
|
||||
Full documentation for the `CallbackQuery
|
||||
<telethon.events.callbackquery.CallbackQuery>`.
|
||||
|
||||
.. currentmodule:: telethon.events.callbackquery.CallbackQuery.Event
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
id
|
||||
message_id
|
||||
data
|
||||
chat_instance
|
||||
via_inline
|
||||
|
||||
respond
|
||||
reply
|
||||
edit
|
||||
delete
|
||||
answer
|
||||
get_message
|
||||
|
||||
InlineQuery
|
||||
===========
|
||||
|
||||
Occurs whenever you sign in as a bot and a user
|
||||
sends an inline query such as ``@bot query``.
|
||||
|
||||
Full documentation for the `InlineQuery
|
||||
<telethon.events.inlinequery.InlineQuery>`.
|
||||
|
||||
.. currentmodule:: telethon.events.inlinequery.InlineQuery.Event
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
id
|
||||
text
|
||||
offset
|
||||
geo
|
||||
builder
|
||||
|
||||
answer
|
||||
|
||||
Album
|
||||
=====
|
||||
|
||||
Occurs whenever you receive an entire album.
|
||||
|
||||
Full documentation for the `Album
|
||||
<telethon.events.album.Album>`.
|
||||
|
||||
.. currentmodule:: telethon.events.album.Album.Event
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
grouped_id
|
||||
text
|
||||
raw_text
|
||||
is_reply
|
||||
forward
|
||||
|
||||
get_reply_message
|
||||
respond
|
||||
reply
|
||||
forward_to
|
||||
edit
|
||||
delete
|
||||
mark_read
|
||||
pin
|
||||
|
||||
Raw
|
||||
===
|
||||
|
||||
Raw events are not actual events. Instead, they are the raw
|
||||
:tl:`Update` object that Telegram sends. You normally shouldn't
|
||||
need these.
|
|
@ -1,423 +0,0 @@
|
|||
.. _faq:
|
||||
|
||||
===
|
||||
FAQ
|
||||
===
|
||||
|
||||
Let's start the quick references section with some useful tips to keep in
|
||||
mind, with the hope that you will understand why certain things work the
|
||||
way that they do.
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
Code without errors doesn't work
|
||||
================================
|
||||
|
||||
Then it probably has errors, but you haven't enabled logging yet.
|
||||
To enable logging, at the following code to the top of your main file:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import logging
|
||||
logging.basicConfig(format='[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s',
|
||||
level=logging.WARNING)
|
||||
|
||||
You can change the logging level to be something different, from less to more information:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
level=logging.CRITICAL # won't show errors (same as disabled)
|
||||
level=logging.ERROR # will only show errors that you didn't handle
|
||||
level=logging.WARNING # will also show messages with medium severity, such as internal Telegram issues
|
||||
level=logging.INFO # will also show informational messages, such as connection or disconnections
|
||||
level=logging.DEBUG # will show a lot of output to help debugging issues in the library
|
||||
|
||||
See the official Python documentation for more information on logging_.
|
||||
|
||||
|
||||
How can I except FloodWaitError?
|
||||
================================
|
||||
|
||||
You can use all errors from the API by importing:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import errors
|
||||
|
||||
And except them as such:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
try:
|
||||
await client.send_message(chat, 'Hi')
|
||||
except errors.FloodWaitError as e:
|
||||
# e.seconds is how many seconds you have
|
||||
# to wait before making the request again.
|
||||
print('Flood for', e.seconds)
|
||||
|
||||
|
||||
My account was deleted/limited when using the library
|
||||
=====================================================
|
||||
|
||||
First and foremost, **this is not a problem exclusive to Telethon.
|
||||
Any third-party library is prone to cause the accounts to appear banned.**
|
||||
Even official applications can make Telegram ban an account under certain
|
||||
circumstances. Third-party libraries such as Telethon are a lot easier to
|
||||
use, and as such, they are misused to spam, which causes Telegram to learn
|
||||
certain patterns and ban suspicious activity.
|
||||
|
||||
There is no point in Telethon trying to circumvent this. Even if it succeeded,
|
||||
spammers would then abuse the library again, and the cycle would repeat.
|
||||
|
||||
The library will only do things that you tell it to do. If you use
|
||||
the library with bad intentions, Telegram will hopefully ban you.
|
||||
|
||||
However, you may also be part of a limited country, such as Iran or Russia.
|
||||
In that case, 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.
|
||||
|
||||
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.
|
||||
|
||||
More recently (year 2023 onwards), Telegram has started putting a lot more
|
||||
measures to prevent spam (with even additions such as anonymous participants
|
||||
in groups or the inability to fetch group members at all). This means some
|
||||
of the anti-spam measures have gotten more aggressive.
|
||||
|
||||
The recommendation has usually been to use the library only on well-established
|
||||
accounts (and not an account you just created), and to not perform actions that
|
||||
could be seen as abuse. Telegram decides what those actions are, and they're
|
||||
free to change how they operate at any time.
|
||||
|
||||
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`_.
|
||||
|
||||
|
||||
How can I use a proxy?
|
||||
======================
|
||||
|
||||
This was one of the first things described in :ref:`signing-in`.
|
||||
|
||||
|
||||
How do I access a field?
|
||||
========================
|
||||
|
||||
This is basic Python knowledge. You should use the dot operator:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
me = await client.get_me()
|
||||
print(me.username)
|
||||
# ^ we used the dot operator to access the username attribute
|
||||
|
||||
result = await client(functions.photos.GetUserPhotosRequest(
|
||||
user_id='me',
|
||||
offset=0,
|
||||
max_id=0,
|
||||
limit=100
|
||||
))
|
||||
|
||||
# Working with list is also pretty basic
|
||||
print(result.photos[0].sizes[-1].type)
|
||||
# ^ ^ ^ ^ ^
|
||||
# | | | | \ type
|
||||
# | | | \ last size
|
||||
# | | \ list of sizes
|
||||
# access | \ first photo from the list
|
||||
# the... \ list of photos
|
||||
#
|
||||
# To print all, you could do (or mix-and-match):
|
||||
for photo in result.photos:
|
||||
for size in photo.sizes:
|
||||
print(size.type)
|
||||
|
||||
|
||||
AttributeError: 'coroutine' object has no attribute 'id'
|
||||
========================================================
|
||||
|
||||
You either forgot to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import telethon.sync
|
||||
# ^^^^^ import sync
|
||||
|
||||
Or:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def handler(event):
|
||||
me = await client.get_me()
|
||||
# ^^^^^ note the await
|
||||
print(me.username)
|
||||
|
||||
|
||||
sqlite3.OperationalError: database is locked
|
||||
============================================
|
||||
|
||||
An older process is still running and is using the same ``'session'`` file.
|
||||
|
||||
This error occurs when **two or more clients use the same session**,
|
||||
that is, when you write the same session name to be used in the client:
|
||||
|
||||
* You have an older process using the same session file.
|
||||
* You have two different scripts running (interactive sessions count too).
|
||||
* You have two clients in the same script running at the same time.
|
||||
|
||||
The solution is, if you need two clients, use two sessions. If the
|
||||
problem persists and you're on Linux, you can use ``fuser my.session``
|
||||
to find out the process locking the file. As a last resort, you can
|
||||
reboot your system.
|
||||
|
||||
If you really dislike SQLite, use a different session storage. There
|
||||
is an entire section covering that at :ref:`sessions`.
|
||||
|
||||
|
||||
event.chat or event.sender is None
|
||||
==================================
|
||||
|
||||
Telegram doesn't always send this information in order to save bandwidth.
|
||||
If you need the information, you should fetch it yourself, since the library
|
||||
won't do unnecessary work unless you need to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async def handler(event):
|
||||
chat = await event.get_chat()
|
||||
sender = await event.get_sender()
|
||||
|
||||
|
||||
File download is slow or sending files takes too long
|
||||
=====================================================
|
||||
|
||||
The communication with Telegram is encrypted. Encryption requires a lot of
|
||||
math, and doing it in pure Python is very slow. ``cryptg`` is a library which
|
||||
containns the encryption functions used by Telethon. If it is installed (via
|
||||
``pip install cryptg``), it will automatically be used and should provide
|
||||
a considerable speed boost. You can know whether it's used by configuring
|
||||
``logging`` (at ``INFO`` level or lower) *before* importing ``telethon``.
|
||||
|
||||
Note that the library does *not* download or upload files in parallel, which
|
||||
can also help with the speed of downloading or uploading a single file. There
|
||||
are snippets online implementing that. The reason why this is not built-in
|
||||
is because the limiting factor in the long run are ``FloodWaitError``, and
|
||||
using parallel download or uploads only makes them occur sooner.
|
||||
|
||||
|
||||
What does "Server sent a very new message with ID" mean?
|
||||
========================================================
|
||||
|
||||
You may also see this error as "Server sent a very old message with ID".
|
||||
|
||||
This is a security feature from Telethon that cannot be disabled and is
|
||||
meant to protect you against replay attacks.
|
||||
|
||||
When this message is incorrectly reported as a "bug",
|
||||
the most common patterns seem to be:
|
||||
|
||||
* Your system time is incorrect.
|
||||
* The proxy you're using may be interfering somehow.
|
||||
* The Telethon session is being used or has been used from somewhere else.
|
||||
Make sure that you created the session from Telethon, and are not using the
|
||||
same session anywhere else. If you need to use the same account from
|
||||
multiple places, login and use a different session for each place you need.
|
||||
|
||||
|
||||
What does "Server replied with a wrong session ID" mean?
|
||||
========================================================
|
||||
|
||||
This is a security feature from Telethon that cannot be disabled and is
|
||||
meant to protect you against unwanted session reuse.
|
||||
|
||||
When this message is reported as a "bug", the most common patterns seem to be:
|
||||
|
||||
* The proxy you're using may be interfering somehow.
|
||||
* The Telethon session is being used or has been used from somewhere else.
|
||||
Make sure that you created the session from Telethon, and are not using the
|
||||
same session anywhere else. If you need to use the same account from
|
||||
multiple places, login and use a different session for each place you need.
|
||||
* You may be using multiple connections to the Telegram server, which seems
|
||||
to confuse Telegram.
|
||||
|
||||
Most of the time it should be safe to ignore this warning. If the library
|
||||
still doesn't behave correctly, make sure to check if any of the above bullet
|
||||
points applies in your case and try to work around it.
|
||||
|
||||
If the issue persists and there is a way to reliably reproduce this error,
|
||||
please add a comment with any additional details you can provide to
|
||||
`issue 3759`_, and perhaps some additional investigation can be done
|
||||
(but it's unlikely, as Telegram *is* sending unexpected data).
|
||||
|
||||
|
||||
What does "Could not find a matching Constructor ID for the TLObject" mean?
|
||||
===========================================================================
|
||||
|
||||
Telegram uses "layers", which you can think of as "versions" of the API they
|
||||
offer. When Telethon reads responses that the Telegram servers send, these
|
||||
need to be deserialized (into what Telethon calls "TLObjects").
|
||||
|
||||
Every Telethon version understands a single Telegram layer. When Telethon
|
||||
connects to Telegram, both agree on the layer to use. If the layers don't
|
||||
match, Telegram may send certain objects which Telethon no longer understands.
|
||||
|
||||
When this message is reported as a "bug", the most common patterns seem to be
|
||||
that he Telethon session is being used or has been used from somewhere else.
|
||||
Make sure that you created the session from Telethon, and are not using the
|
||||
same session anywhere else. If you need to use the same account from
|
||||
multiple places, login and use a different session for each place you need.
|
||||
|
||||
|
||||
What does "Task was destroyed but it is pending" mean?
|
||||
======================================================
|
||||
|
||||
Your script likely finished abruptly, the ``asyncio`` event loop got
|
||||
destroyed, and the library did not get a chance to properly close the
|
||||
connection and close the session.
|
||||
|
||||
Make sure you're either using the context manager for the client or always
|
||||
call ``await client.disconnect()`` (by e.g. using a ``try/finally``).
|
||||
|
||||
|
||||
What does "The asyncio event loop must not change after connection" mean?
|
||||
=========================================================================
|
||||
|
||||
Telethon uses ``asyncio``, and makes use of things like tasks and queues
|
||||
internally to manage the connection to the server and match responses to the
|
||||
requests you make. Most of them are initialized after the client is connected.
|
||||
|
||||
For example, if the library expects a result to a request made in loop A, but
|
||||
you attempt to get that result in loop B, you will very likely find a deadlock.
|
||||
To avoid a deadlock, the library checks to make sure the loop in use is the
|
||||
same as the one used to initialize everything, and if not, it throws an error.
|
||||
|
||||
The most common cause is ``asyncio.run``, since it creates a new event loop.
|
||||
If you ``asyncio.run`` a function to create the client and set it up, and then
|
||||
you ``asyncio.run`` another function to do work, things won't work, so the
|
||||
library throws an error early to let you know something is wrong.
|
||||
|
||||
Instead, it's often a good idea to have a single ``async def main`` and simply
|
||||
``asyncio.run()`` it and do all the work there. From it, you're also able to
|
||||
call other ``async def`` without having to touch ``asyncio.run`` again:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# It's fine to create the client outside as long as you don't connect
|
||||
client = TelegramClient(...)
|
||||
|
||||
async def main():
|
||||
# Now the client will connect, so the loop must not change from now on.
|
||||
# But as long as you do all the work inside main, including calling
|
||||
# other async functions, things will work.
|
||||
async with client:
|
||||
....
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
||||
Be sure to read the ``asyncio`` documentation if you want a better
|
||||
understanding of event loop, tasks, and what functions you can use.
|
||||
|
||||
|
||||
What does "bases ChatGetter" mean?
|
||||
==================================
|
||||
|
||||
In Python, classes can base others. This is called `inheritance
|
||||
<https://ddg.gg/python%20inheritance>`_. What it means is that
|
||||
"if a class bases another, you can use the other's methods too".
|
||||
|
||||
For example, `Message <telethon.tl.custom.message.Message>` *bases*
|
||||
`ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`. In turn,
|
||||
`ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>` defines
|
||||
things like `obj.chat_id <telethon.tl.custom.chatgetter.ChatGetter>`.
|
||||
|
||||
So if you have a message, you can access that too:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# ChatGetter has a chat_id property, and Message bases ChatGetter.
|
||||
# Thus you can use ChatGetter properties and methods from Message
|
||||
print(message.chat_id)
|
||||
|
||||
|
||||
Telegram has a lot to offer, and inheritance helps the library reduce
|
||||
boilerplate, so it's important to know this concept. For newcomers,
|
||||
this may be a problem, so we explain what it means here in the FAQ.
|
||||
|
||||
Can I send files by ID?
|
||||
=======================
|
||||
|
||||
When people talk about IDs, they often refer to one of two things:
|
||||
the integer ID inside media, and a random-looking long string.
|
||||
|
||||
You cannot use the integer ID to send media. Generally speaking, sending media
|
||||
requires a combination of ID, ``access_hash`` and ``file_reference``.
|
||||
The first two are integers, while the last one is a random ``bytes`` sequence.
|
||||
|
||||
* The integer ``id`` will always be the same for every account, so every user
|
||||
or bot looking at a particular media file, will see a consistent ID.
|
||||
* The ``access_hash`` will always be the same for a given account, but
|
||||
different accounts will each see their own, different ``access_hash``.
|
||||
This makes it impossible to get media object from one account and use it in
|
||||
another. The other account must fetch the media object itself.
|
||||
* The ``file_reference`` is random for everyone and will only work for a few
|
||||
hours before it expires. It must be refetched before the media can be used
|
||||
(to either resend the media or download it).
|
||||
|
||||
The second type of "`file ID <https://core.telegram.org/bots/api#inputfile>`_"
|
||||
people refer to is a concept from the HTTP Bot API. It's a custom format which
|
||||
encodes enough information to use the media.
|
||||
|
||||
Telethon provides an old version of these HTTP Bot API-style file IDs via
|
||||
``message.file.id``, however, this feature is no longer maintained, so it may
|
||||
not work. It will be removed in future versions. Nonetheless, it is possible
|
||||
to find a different Python package (or write your own) to parse these file IDs
|
||||
and construct the necessary input file objects to send or download the media.
|
||||
|
||||
|
||||
Can I use Flask with the library?
|
||||
=================================
|
||||
|
||||
Yes, if you know what you are doing. However, you will probably have a
|
||||
lot of headaches to get threads and asyncio to work together. Instead,
|
||||
consider using `Quart <https://pgjones.gitlab.io/quart/>`_, an asyncio-based
|
||||
alternative to `Flask <flask.pocoo.org/>`_.
|
||||
|
||||
Check out `quart_login.py`_ for an example web-application based on Quart.
|
||||
|
||||
Can I use Anaconda/Spyder/IPython with the library?
|
||||
===================================================
|
||||
|
||||
Yes, but these interpreters run the asyncio event loop implicitly,
|
||||
which interferes with the ``telethon.sync`` magic module.
|
||||
|
||||
If you use them, you should **not** import ``sync``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Change any of these...:
|
||||
from telethon import TelegramClient, sync, ...
|
||||
from telethon.sync import TelegramClient, ...
|
||||
|
||||
# ...with this:
|
||||
from telethon import TelegramClient, ...
|
||||
|
||||
You are also more likely to get "sqlite3.OperationalError: database is locked"
|
||||
with them. If they cause too much trouble, just write your code in a ``.py``
|
||||
file and run that, or use the normal ``python`` interpreter.
|
||||
|
||||
.. _logging: https://docs.python.org/3/library/logging.html
|
||||
.. _@SpamBot: https://t.me/SpamBot
|
||||
.. _issue 297: https://github.com/LonamiWebs/Telethon/issues/297
|
||||
.. _issue 3759: https://github.com/LonamiWebs/Telethon/issues/3759
|
||||
.. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/v1/telethon_examples#quart_loginpy
|
|
@ -1,353 +0,0 @@
|
|||
=================
|
||||
Objects Reference
|
||||
=================
|
||||
|
||||
This is the quick reference for those objects returned by client methods
|
||||
or other useful modules that the library has to offer. They are kept in
|
||||
a separate page to help finding and discovering them.
|
||||
|
||||
Remember that this page only shows properties and methods,
|
||||
**not attributes**. Make sure to open the full documentation
|
||||
to find out about the attributes.
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
ChatGetter
|
||||
==========
|
||||
|
||||
All events base `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`,
|
||||
and some of the objects below do too, so it's important to know its methods.
|
||||
|
||||
.. currentmodule:: telethon.tl.custom.chatgetter.ChatGetter
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
chat
|
||||
input_chat
|
||||
chat_id
|
||||
is_private
|
||||
is_group
|
||||
is_channel
|
||||
|
||||
get_chat
|
||||
get_input_chat
|
||||
|
||||
|
||||
SenderGetter
|
||||
============
|
||||
|
||||
Similar to `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`, a
|
||||
`SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>` is the same,
|
||||
but it works for senders instead.
|
||||
|
||||
.. currentmodule:: telethon.tl.custom.sendergetter.SenderGetter
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
sender
|
||||
input_sender
|
||||
sender_id
|
||||
|
||||
get_sender
|
||||
get_input_sender
|
||||
|
||||
|
||||
Message
|
||||
=======
|
||||
|
||||
.. currentmodule:: telethon.tl.custom.message
|
||||
|
||||
The `Message` type is very important, mostly because we are working
|
||||
with a library for a *messaging* platform, so messages are widely used:
|
||||
in events, when fetching history, replies, etc.
|
||||
|
||||
It bases `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>` and
|
||||
`SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>`.
|
||||
|
||||
Properties
|
||||
----------
|
||||
|
||||
.. note::
|
||||
|
||||
We document *custom properties* here, not all the attributes of the
|
||||
`Message` (which is the information Telegram actually returns).
|
||||
|
||||
.. currentmodule:: telethon.tl.custom.message.Message
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
text
|
||||
raw_text
|
||||
is_reply
|
||||
forward
|
||||
buttons
|
||||
button_count
|
||||
file
|
||||
photo
|
||||
document
|
||||
web_preview
|
||||
audio
|
||||
voice
|
||||
video
|
||||
video_note
|
||||
gif
|
||||
sticker
|
||||
contact
|
||||
game
|
||||
geo
|
||||
invoice
|
||||
poll
|
||||
venue
|
||||
action_entities
|
||||
via_bot
|
||||
via_input_bot
|
||||
client
|
||||
|
||||
|
||||
Methods
|
||||
-------
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
respond
|
||||
reply
|
||||
forward_to
|
||||
edit
|
||||
delete
|
||||
get_reply_message
|
||||
click
|
||||
mark_read
|
||||
pin
|
||||
download_media
|
||||
get_entities_text
|
||||
get_buttons
|
||||
|
||||
|
||||
File
|
||||
====
|
||||
|
||||
The `File <telethon.tl.custom.file.File>` type is a wrapper object
|
||||
returned by `Message.file <telethon.tl.custom.message.Message.file>`,
|
||||
and you can use it to easily access a document's attributes, such as
|
||||
its name, bot-API style file ID, etc.
|
||||
|
||||
.. currentmodule:: telethon.tl.custom.file.File
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
id
|
||||
name
|
||||
ext
|
||||
mime_type
|
||||
width
|
||||
height
|
||||
size
|
||||
duration
|
||||
title
|
||||
performer
|
||||
emoji
|
||||
sticker_set
|
||||
|
||||
|
||||
Conversation
|
||||
============
|
||||
|
||||
The `Conversation <telethon.tl.custom.conversation.Conversation>` object
|
||||
is returned by the `client.conversation()
|
||||
<telethon.client.dialogs.DialogMethods.conversation>` method to easily
|
||||
send and receive responses like a normal conversation.
|
||||
|
||||
It bases `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`.
|
||||
|
||||
.. currentmodule:: telethon.tl.custom.conversation.Conversation
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
send_message
|
||||
send_file
|
||||
mark_read
|
||||
get_response
|
||||
get_reply
|
||||
get_edit
|
||||
wait_read
|
||||
wait_event
|
||||
cancel
|
||||
cancel_all
|
||||
|
||||
|
||||
AdminLogEvent
|
||||
=============
|
||||
|
||||
The `AdminLogEvent <telethon.tl.custom.adminlogevent.AdminLogEvent>` object
|
||||
is returned by the `client.iter_admin_log()
|
||||
<telethon.client.chats.ChatMethods.iter_admin_log>` method to easily iterate
|
||||
over past "events" (deleted messages, edits, title changes, leaving members…)
|
||||
|
||||
These are all the properties you can find in it:
|
||||
|
||||
.. currentmodule:: telethon.tl.custom.adminlogevent.AdminLogEvent
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
id
|
||||
date
|
||||
user_id
|
||||
action
|
||||
old
|
||||
new
|
||||
changed_about
|
||||
changed_title
|
||||
changed_username
|
||||
changed_photo
|
||||
changed_sticker_set
|
||||
changed_message
|
||||
deleted_message
|
||||
changed_admin
|
||||
changed_restrictions
|
||||
changed_invites
|
||||
joined
|
||||
joined_invite
|
||||
left
|
||||
changed_hide_history
|
||||
changed_signatures
|
||||
changed_pin
|
||||
changed_default_banned_rights
|
||||
stopped_poll
|
||||
|
||||
|
||||
Button
|
||||
======
|
||||
|
||||
The `Button <telethon.tl.custom.button.Button>` class is used when you login
|
||||
as a bot account to send messages with reply markup, such as inline buttons
|
||||
or custom keyboards.
|
||||
|
||||
These are the static methods you can use to create instances of the markup:
|
||||
|
||||
.. currentmodule:: telethon.tl.custom.button.Button
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
inline
|
||||
switch_inline
|
||||
url
|
||||
auth
|
||||
text
|
||||
request_location
|
||||
request_phone
|
||||
request_poll
|
||||
clear
|
||||
force_reply
|
||||
|
||||
|
||||
InlineResult
|
||||
============
|
||||
|
||||
The `InlineResult <telethon.tl.custom.inlineresult.InlineResult>` object
|
||||
is returned inside a list by the `client.inline_query()
|
||||
<telethon.client.bots.BotMethods.inline_query>` method to make an inline
|
||||
query to a bot that supports being used in inline mode, such as
|
||||
`@like <https://t.me/like>`_.
|
||||
|
||||
Note that the list returned is in fact a *subclass* of a list called
|
||||
`InlineResults <telethon.tl.custom.inlineresults.InlineResults>`, which,
|
||||
in addition of being a list (iterator, indexed access, etc.), has extra
|
||||
attributes and methods.
|
||||
|
||||
These are the constants for the types, properties and methods you
|
||||
can find the individual results:
|
||||
|
||||
.. currentmodule:: telethon.tl.custom.inlineresult.InlineResult
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
ARTICLE
|
||||
PHOTO
|
||||
GIF
|
||||
VIDEO
|
||||
VIDEO_GIF
|
||||
AUDIO
|
||||
DOCUMENT
|
||||
LOCATION
|
||||
VENUE
|
||||
CONTACT
|
||||
GAME
|
||||
type
|
||||
message
|
||||
title
|
||||
description
|
||||
url
|
||||
photo
|
||||
document
|
||||
click
|
||||
download_media
|
||||
|
||||
|
||||
Dialog
|
||||
======
|
||||
|
||||
The `Dialog <telethon.tl.custom.dialog.Dialog>` object is returned when
|
||||
you call `client.iter_dialogs() <telethon.client.dialogs.DialogMethods.iter_dialogs>`.
|
||||
|
||||
.. currentmodule:: telethon.tl.custom.dialog.Dialog
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
send_message
|
||||
archive
|
||||
delete
|
||||
|
||||
|
||||
Draft
|
||||
======
|
||||
|
||||
The `Draft <telethon.tl.custom.draft.Draft>` object is returned when
|
||||
you call `client.iter_drafts() <telethon.client.dialogs.DialogMethods.iter_drafts>`.
|
||||
|
||||
.. currentmodule:: telethon.tl.custom.draft.Draft
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
entity
|
||||
input_entity
|
||||
get_entity
|
||||
get_input_entity
|
||||
text
|
||||
raw_text
|
||||
is_empty
|
||||
set_message
|
||||
send
|
||||
delete
|
||||
|
||||
|
||||
Utils
|
||||
=====
|
||||
|
||||
The `telethon.utils` module has plenty of methods that make using the
|
||||
library a lot easier. Only the interesting ones will be listed here.
|
||||
|
||||
.. currentmodule:: telethon.utils
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
|
||||
get_display_name
|
||||
get_extension
|
||||
get_inner_text
|
||||
get_peer_id
|
||||
resolve_id
|
||||
pack_bot_file_id
|
||||
resolve_bot_file_id
|
||||
resolve_invite_link
|
|
@ -1 +0,0 @@
|
|||
telethon
|
|
@ -1,2 +0,0 @@
|
|||
pyaes
|
||||
rsa
|
263
setup.py
263
setup.py
|
@ -1,263 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""A setuptools based setup module.
|
||||
|
||||
See:
|
||||
https://packaging.python.org/en/latest/distributing.html
|
||||
https://github.com/pypa/sampleproject
|
||||
|
||||
Extra supported commands are:
|
||||
* gen, to generate the classes required for Telethon to run or docs
|
||||
* pypi, to generate sdist, bdist_wheel, and push to PyPi
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
# Needed since we're importing local files
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
class TempWorkDir:
|
||||
"""Switches the working directory to be the one on which this file lives,
|
||||
while within the 'with' block.
|
||||
"""
|
||||
def __init__(self, new=None):
|
||||
self.original = None
|
||||
self.new = new or str(Path(__file__).parent.resolve())
|
||||
|
||||
def __enter__(self):
|
||||
# os.chdir does not work with Path in Python 3.5.x
|
||||
self.original = str(Path('.').resolve())
|
||||
os.makedirs(self.new, exist_ok=True)
|
||||
os.chdir(self.new)
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
os.chdir(self.original)
|
||||
|
||||
|
||||
API_REF_URL = 'https://tl.telethon.dev/'
|
||||
|
||||
GENERATOR_DIR = Path('telethon_generator')
|
||||
LIBRARY_DIR = Path('telethon')
|
||||
|
||||
ERRORS_IN = GENERATOR_DIR / 'data/errors.csv'
|
||||
ERRORS_OUT = LIBRARY_DIR / 'errors/rpcerrorlist.py'
|
||||
|
||||
METHODS_IN = GENERATOR_DIR / 'data/methods.csv'
|
||||
|
||||
# Which raw API methods are covered by *friendly* methods in the client?
|
||||
FRIENDLY_IN = GENERATOR_DIR / 'data/friendly.csv'
|
||||
|
||||
TLOBJECT_IN_TLS = [Path(x) for x in GENERATOR_DIR.glob('data/*.tl')]
|
||||
TLOBJECT_OUT = LIBRARY_DIR / 'tl'
|
||||
IMPORT_DEPTH = 2
|
||||
|
||||
DOCS_IN_RES = GENERATOR_DIR / 'data/html'
|
||||
DOCS_OUT = Path('docs')
|
||||
|
||||
|
||||
def generate(which, action='gen'):
|
||||
from telethon_generator.parsers import\
|
||||
parse_errors, parse_methods, parse_tl, find_layer
|
||||
|
||||
from telethon_generator.generators import\
|
||||
generate_errors, generate_tlobjects, generate_docs, clean_tlobjects
|
||||
|
||||
layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS)))
|
||||
errors = list(parse_errors(ERRORS_IN))
|
||||
methods = list(parse_methods(METHODS_IN, FRIENDLY_IN, {e.str_code: e for e in errors}))
|
||||
|
||||
tlobjects = list(itertools.chain(*(
|
||||
parse_tl(file, layer, methods) for file in TLOBJECT_IN_TLS)))
|
||||
|
||||
if not which:
|
||||
which.extend(('tl', 'errors'))
|
||||
|
||||
clean = action == 'clean'
|
||||
action = 'Cleaning' if clean else 'Generating'
|
||||
|
||||
if 'all' in which:
|
||||
which.remove('all')
|
||||
for x in ('tl', 'errors', 'docs'):
|
||||
if x not in which:
|
||||
which.append(x)
|
||||
|
||||
if 'tl' in which:
|
||||
which.remove('tl')
|
||||
print(action, 'TLObjects...')
|
||||
if clean:
|
||||
clean_tlobjects(TLOBJECT_OUT)
|
||||
else:
|
||||
generate_tlobjects(tlobjects, layer, IMPORT_DEPTH, TLOBJECT_OUT)
|
||||
|
||||
if 'errors' in which:
|
||||
which.remove('errors')
|
||||
print(action, 'RPCErrors...')
|
||||
if clean:
|
||||
if ERRORS_OUT.is_file():
|
||||
ERRORS_OUT.unlink()
|
||||
else:
|
||||
with ERRORS_OUT.open('w') as file:
|
||||
generate_errors(errors, file)
|
||||
|
||||
if 'docs' in which:
|
||||
which.remove('docs')
|
||||
print(action, 'documentation...')
|
||||
if clean:
|
||||
if DOCS_OUT.is_dir():
|
||||
shutil.rmtree(str(DOCS_OUT))
|
||||
else:
|
||||
in_path = DOCS_IN_RES.resolve()
|
||||
with TempWorkDir(DOCS_OUT):
|
||||
generate_docs(tlobjects, methods, layer, in_path)
|
||||
|
||||
if 'json' in which:
|
||||
which.remove('json')
|
||||
print(action, 'JSON schema...')
|
||||
json_files = [x.with_suffix('.json') for x in TLOBJECT_IN_TLS]
|
||||
if clean:
|
||||
for file in json_files:
|
||||
if file.is_file():
|
||||
file.unlink()
|
||||
else:
|
||||
def gen_json(fin, fout):
|
||||
meths = []
|
||||
constructors = []
|
||||
for tl in parse_tl(fin, layer):
|
||||
if tl.is_function:
|
||||
meths.append(tl.to_dict())
|
||||
else:
|
||||
constructors.append(tl.to_dict())
|
||||
what = {'constructors': constructors, 'methods': meths}
|
||||
with open(fout, 'w') as f:
|
||||
json.dump(what, f, indent=2)
|
||||
|
||||
for fs in zip(TLOBJECT_IN_TLS, json_files):
|
||||
gen_json(*fs)
|
||||
|
||||
if which:
|
||||
print(
|
||||
'The following items were not understood:', which,
|
||||
'\n Consider using only "tl", "errors" and/or "docs".'
|
||||
'\n Using only "clean" will clean them. "all" to act on all.'
|
||||
'\n For instance "gen tl errors".'
|
||||
)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) >= 2 and argv[1] in ('gen', 'clean'):
|
||||
generate(argv[2:], argv[1])
|
||||
|
||||
elif len(argv) >= 2 and argv[1] == 'pypi':
|
||||
# Make sure tl.telethon.dev is up-to-date first
|
||||
with urllib.request.urlopen(API_REF_URL) as resp:
|
||||
html = resp.read()
|
||||
m = re.search(br'layer\s+(\d+)', html)
|
||||
if not m:
|
||||
print('Failed to check that the API reference is up to date:', API_REF_URL)
|
||||
return
|
||||
|
||||
from telethon_generator.parsers import find_layer
|
||||
layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS)))
|
||||
published_layer = int(m[1])
|
||||
if published_layer != layer:
|
||||
print('Published layer', published_layer, 'does not match current layer', layer, '.')
|
||||
print('Make sure to update the API reference site first:', API_REF_URL)
|
||||
return
|
||||
|
||||
# (Re)generate the code to make sure we don't push without it
|
||||
generate(['tl', 'errors'])
|
||||
|
||||
# Try importing the telethon module to assert it has no errors
|
||||
try:
|
||||
import telethon
|
||||
except:
|
||||
print('Packaging for PyPi aborted, importing the module failed.')
|
||||
return
|
||||
|
||||
remove_dirs = ['__pycache__', 'build', 'dist', 'Telethon.egg-info']
|
||||
for root, _dirs, _files in os.walk(LIBRARY_DIR, topdown=False):
|
||||
# setuptools is including __pycache__ for some reason (#1605)
|
||||
if root.endswith('/__pycache__'):
|
||||
remove_dirs.append(root)
|
||||
for x in remove_dirs:
|
||||
shutil.rmtree(x, ignore_errors=True)
|
||||
|
||||
run('python3 setup.py sdist', shell=True)
|
||||
run('python3 setup.py bdist_wheel', shell=True)
|
||||
run('twine upload dist/*', shell=True)
|
||||
for x in ('build', 'dist', 'Telethon.egg-info'):
|
||||
shutil.rmtree(x, ignore_errors=True)
|
||||
|
||||
else:
|
||||
# e.g. install from GitHub
|
||||
if GENERATOR_DIR.is_dir():
|
||||
generate(['tl', 'errors'])
|
||||
|
||||
# Get the long description from the README file
|
||||
with open('README.rst', 'r', encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
with open('telethon/version.py', 'r', encoding='utf-8') as f:
|
||||
version = re.search(r"^__version__\s*=\s*'(.*)'.*$",
|
||||
f.read(), flags=re.MULTILINE).group(1)
|
||||
setup(
|
||||
name='Telethon',
|
||||
version=version,
|
||||
description="Full-featured Telegram client library for Python 3",
|
||||
long_description=long_description,
|
||||
|
||||
url='https://github.com/LonamiWebs/Telethon',
|
||||
download_url='https://github.com/LonamiWebs/Telethon/releases',
|
||||
|
||||
author='Lonami Exo',
|
||||
author_email='totufals@hotmail.com',
|
||||
|
||||
license='MIT',
|
||||
|
||||
# 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',
|
||||
|
||||
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers=[
|
||||
# 3 - Alpha
|
||||
# 4 - Beta
|
||||
# 5 - Production/Stable
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
|
||||
'Intended Audience :: Developers',
|
||||
'Topic :: Communications :: Chat',
|
||||
|
||||
'License :: OSI Approved :: MIT License',
|
||||
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
],
|
||||
keywords='telegram api chat client library messaging mtproto',
|
||||
packages=find_packages(exclude=[
|
||||
'telethon_*', 'tests*'
|
||||
]),
|
||||
install_requires=['pyaes', 'rsa'],
|
||||
extras_require={
|
||||
'cryptg': ['cryptg']
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
with TempWorkDir():
|
||||
main(sys.argv)
|
|
@ -1,13 +0,0 @@
|
|||
from .client.telegramclient import TelegramClient
|
||||
from .network import connection
|
||||
from .tl.custom import Button
|
||||
from .tl import patched as _ # import for its side-effects
|
||||
from . import version, events, utils, errors, types, functions, custom
|
||||
|
||||
__version__ = version.__version__
|
||||
|
||||
__all__ = [
|
||||
'TelegramClient', 'Button',
|
||||
'types', 'functions', 'custom', 'errors',
|
||||
'events', 'utils', 'connection'
|
||||
]
|
|
@ -1,3 +0,0 @@
|
|||
from .entitycache import EntityCache
|
||||
from .messagebox import MessageBox, GapError, PrematureEndReason
|
||||
from .session import SessionState, ChannelState, Entity, EntityType
|
|
@ -1,62 +0,0 @@
|
|||
from .session import EntityType, Entity
|
||||
|
||||
|
||||
_sentinel = object()
|
||||
|
||||
|
||||
class EntityCache:
|
||||
def __init__(
|
||||
self,
|
||||
hash_map: dict = _sentinel,
|
||||
self_id: int = None,
|
||||
self_bot: bool = None
|
||||
):
|
||||
self.hash_map = {} if hash_map is _sentinel else hash_map
|
||||
self.self_id = self_id
|
||||
self.self_bot = self_bot
|
||||
|
||||
def set_self_user(self, id, bot, hash):
|
||||
self.self_id = id
|
||||
self.self_bot = bot
|
||||
if hash:
|
||||
self.hash_map[id] = (hash, EntityType.BOT if bot else EntityType.USER)
|
||||
|
||||
def get(self, id):
|
||||
try:
|
||||
hash, ty = self.hash_map[id]
|
||||
return Entity(ty, id, hash)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def extend(self, users, chats):
|
||||
# See https://core.telegram.org/api/min for "issues" with "min constructors".
|
||||
self.hash_map.update(
|
||||
(u.id, (
|
||||
u.access_hash,
|
||||
EntityType.BOT if u.bot else EntityType.USER,
|
||||
))
|
||||
for u in users
|
||||
if getattr(u, 'access_hash', None) and not u.min
|
||||
)
|
||||
self.hash_map.update(
|
||||
(c.id, (
|
||||
c.access_hash,
|
||||
EntityType.MEGAGROUP if c.megagroup else (
|
||||
EntityType.GIGAGROUP if getattr(c, 'gigagroup', None) else EntityType.CHANNEL
|
||||
),
|
||||
))
|
||||
for c in chats
|
||||
if getattr(c, 'access_hash', None) and not getattr(c, 'min', None)
|
||||
)
|
||||
|
||||
def get_all_entities(self):
|
||||
return [Entity(ty, id, hash) for id, (hash, ty) in self.hash_map.items()]
|
||||
|
||||
def put(self, entity):
|
||||
self.hash_map[entity.id] = (entity.hash, entity.ty)
|
||||
|
||||
def retain(self, filter):
|
||||
self.hash_map = {k: v for k, v in self.hash_map.items() if filter(k)}
|
||||
|
||||
def __len__(self):
|
||||
return len(self.hash_map)
|
|
@ -1,810 +0,0 @@
|
|||
"""
|
||||
This module deals with correct handling of updates, including gaps, and knowing when the code
|
||||
should "get difference" (the set of updates that the client should know by now minus the set
|
||||
of updates that it actually knows).
|
||||
|
||||
Each chat has its own [`Entry`] in the [`MessageBox`] (this `struct` is the "entry point").
|
||||
At any given time, the message box may be either getting difference for them (entry is in
|
||||
[`MessageBox::getting_diff_for`]) or not. If not getting difference, a possible gap may be
|
||||
found for the updates (entry is in [`MessageBox::possible_gaps`]). Otherwise, the entry is
|
||||
on its happy path.
|
||||
|
||||
Gaps are cleared when they are either resolved on their own (by waiting for a short time)
|
||||
or because we got the difference for the corresponding entry.
|
||||
|
||||
While there are entries for which their difference must be fetched,
|
||||
[`MessageBox::check_deadlines`] will always return [`Instant::now`], since "now" is the time
|
||||
to get the difference.
|
||||
"""
|
||||
import asyncio
|
||||
import datetime
|
||||
import time
|
||||
import logging
|
||||
from enum import Enum
|
||||
from .session import SessionState, ChannelState
|
||||
from ..tl import types as tl, functions as fn
|
||||
from ..helpers import get_running_loop
|
||||
|
||||
|
||||
# Telegram sends `seq` equal to `0` when "it doesn't matter", so we use that value too.
|
||||
NO_SEQ = 0
|
||||
|
||||
# See https://core.telegram.org/method/updates.getChannelDifference.
|
||||
BOT_CHANNEL_DIFF_LIMIT = 100000
|
||||
USER_CHANNEL_DIFF_LIMIT = 100
|
||||
|
||||
# > It may be useful to wait up to 0.5 seconds
|
||||
POSSIBLE_GAP_TIMEOUT = 0.5
|
||||
|
||||
# After how long without updates the client will "timeout".
|
||||
#
|
||||
# When this timeout occurs, the client will attempt to fetch updates by itself, ignoring all the
|
||||
# updates that arrive in the meantime. After all updates are fetched when this happens, the
|
||||
# client will resume normal operation, and the timeout will reset.
|
||||
#
|
||||
# Documentation recommends 15 minutes without updates (https://core.telegram.org/api/updates).
|
||||
NO_UPDATES_TIMEOUT = 15 * 60
|
||||
|
||||
# Entry "enum".
|
||||
# Account-wide `pts` includes private conversations (one-to-one) and small group chats.
|
||||
ENTRY_ACCOUNT = object()
|
||||
# Account-wide `qts` includes only "secret" one-to-one chats.
|
||||
ENTRY_SECRET = object()
|
||||
# Integers will be Channel-specific `pts`, and includes "megagroup", "broadcast" and "supergroup" channels.
|
||||
|
||||
# Python's logging doesn't define a TRACE level. Pick halfway between DEBUG and NOTSET.
|
||||
# We don't define a name for this as libraries shouldn't do that though.
|
||||
LOG_LEVEL_TRACE = (logging.DEBUG - logging.NOTSET) // 2
|
||||
|
||||
_sentinel = object()
|
||||
|
||||
def next_updates_deadline():
|
||||
return get_running_loop().time() + NO_UPDATES_TIMEOUT
|
||||
|
||||
|
||||
class GapError(ValueError):
|
||||
def __repr__(self):
|
||||
return 'GapError()'
|
||||
|
||||
|
||||
class PrematureEndReason(Enum):
|
||||
TEMPORARY_SERVER_ISSUES = 'tmp'
|
||||
BANNED = 'ban'
|
||||
|
||||
|
||||
# Represents the information needed to correctly handle a specific `tl::enums::Update`.
|
||||
class PtsInfo:
|
||||
__slots__ = ('pts', 'pts_count', 'entry')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pts: int,
|
||||
pts_count: int,
|
||||
entry: object
|
||||
):
|
||||
self.pts = pts
|
||||
self.pts_count = pts_count
|
||||
self.entry = entry
|
||||
|
||||
@classmethod
|
||||
def from_update(cls, update):
|
||||
pts = getattr(update, 'pts', None)
|
||||
if pts:
|
||||
pts_count = getattr(update, 'pts_count', None) or 0
|
||||
try:
|
||||
entry = update.message.peer_id.channel_id
|
||||
except AttributeError:
|
||||
entry = getattr(update, 'channel_id', None) or ENTRY_ACCOUNT
|
||||
return cls(pts=pts, pts_count=pts_count, entry=entry)
|
||||
|
||||
qts = getattr(update, 'qts', None)
|
||||
if qts:
|
||||
pts_count = 1 if isinstance(update, tl.UpdateNewEncryptedMessage) else 0
|
||||
return cls(pts=qts, pts_count=pts_count, entry=ENTRY_SECRET)
|
||||
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
if self.entry is ENTRY_ACCOUNT:
|
||||
entry = 'ENTRY_ACCOUNT'
|
||||
elif self.entry is ENTRY_SECRET:
|
||||
entry = 'ENTRY_SECRET'
|
||||
else:
|
||||
entry = self.entry
|
||||
return f'PtsInfo(pts={self.pts}, pts_count={self.pts_count}, entry={entry})'
|
||||
|
||||
|
||||
# The state of a particular entry in the message box.
|
||||
class State:
|
||||
__slots__ = ('pts', 'deadline')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
# Current local persistent timestamp.
|
||||
pts: int,
|
||||
# Next instant when we would get the update difference if no updates arrived before then.
|
||||
deadline: float
|
||||
):
|
||||
self.pts = pts
|
||||
self.deadline = deadline
|
||||
|
||||
def __repr__(self):
|
||||
return f'State(pts={self.pts}, deadline={self.deadline})'
|
||||
|
||||
|
||||
# > ### Recovering gaps
|
||||
# > […] Manually obtaining updates is also required in the following situations:
|
||||
# > • Loss of sync: a gap was found in `seq` / `pts` / `qts` (as described above).
|
||||
# > It may be useful to wait up to 0.5 seconds in this situation and abort the sync in case a new update
|
||||
# > arrives, that fills the gap.
|
||||
#
|
||||
# This is really easy to trigger by spamming messages in a channel (with as little as 3 members works), because
|
||||
# the updates produced by the RPC request take a while to arrive (whereas the read update comes faster alone).
|
||||
class PossibleGap:
|
||||
__slots__ = ('deadline', 'updates')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
deadline: float,
|
||||
# Pending updates (those with a larger PTS, producing the gap which may later be filled).
|
||||
updates: list # of updates
|
||||
):
|
||||
self.deadline = deadline
|
||||
self.updates = updates
|
||||
|
||||
def __repr__(self):
|
||||
return f'PossibleGap(deadline={self.deadline}, update_count={len(self.updates)})'
|
||||
|
||||
|
||||
# Represents a "message box" (event `pts` for a specific entry).
|
||||
#
|
||||
# See https://core.telegram.org/api/updates#message-related-event-sequences.
|
||||
class MessageBox:
|
||||
__slots__ = ('_log', 'map', 'date', 'seq', 'next_deadline', 'possible_gaps', 'getting_diff_for', 'reset_deadlines_for')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
log,
|
||||
# Map each entry to their current state.
|
||||
map: dict = _sentinel, # entry -> state
|
||||
|
||||
# Additional fields beyond PTS needed by `ENTRY_ACCOUNT`.
|
||||
date: datetime.datetime = datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc),
|
||||
seq: int = NO_SEQ,
|
||||
|
||||
# Holds the entry with the closest deadline (optimization to avoid recalculating the minimum deadline).
|
||||
next_deadline: object = None, # entry
|
||||
|
||||
# Which entries have a gap and may soon trigger a need to get difference.
|
||||
#
|
||||
# If a gap is found, stores the required information to resolve it (when should it timeout and what updates
|
||||
# should be held in case the gap is resolved on its own).
|
||||
#
|
||||
# Not stored directly in `map` as an optimization (else we would need another way of knowing which entries have
|
||||
# a gap in them).
|
||||
possible_gaps: dict = _sentinel, # entry -> possiblegap
|
||||
|
||||
# For which entries are we currently getting difference.
|
||||
getting_diff_for: set = _sentinel, # entry
|
||||
|
||||
# Temporarily stores which entries should have their update deadline reset.
|
||||
# Stored in the message box in order to reuse the allocation.
|
||||
reset_deadlines_for: set = _sentinel # entry
|
||||
):
|
||||
self._log = log
|
||||
self.map = {} if map is _sentinel else map
|
||||
self.date = date
|
||||
self.seq = seq
|
||||
self.next_deadline = next_deadline
|
||||
self.possible_gaps = {} if possible_gaps is _sentinel else possible_gaps
|
||||
self.getting_diff_for = set() if getting_diff_for is _sentinel else getting_diff_for
|
||||
self.reset_deadlines_for = set() if reset_deadlines_for is _sentinel else reset_deadlines_for
|
||||
|
||||
if __debug__:
|
||||
# Need this to tell them apart when printing the repr of the state map.
|
||||
# Could be done once at the global level, but that makes configuring logging
|
||||
# more annoying because it would need to be done before importing telethon.
|
||||
self._trace('ENTRY_ACCOUNT = %r; ENTRY_SECRET = %r', ENTRY_ACCOUNT, ENTRY_SECRET)
|
||||
self._trace('Created new MessageBox with map = %r, date = %r, seq = %r', self.map, self.date, self.seq)
|
||||
|
||||
def _trace(self, msg, *args, **kwargs):
|
||||
# Calls to trace can't really be removed beforehand without some dark magic.
|
||||
# So every call to trace is prefixed with `if __debug__`` instead, to remove
|
||||
# it when using `python -O`. Probably unnecessary, but it's nice to avoid
|
||||
# paying the cost for something that is not used.
|
||||
self._log.log(LOG_LEVEL_TRACE, msg, *args, **kwargs)
|
||||
|
||||
# region Creation, querying, and setting base state.
|
||||
|
||||
def load(self, session_state, channel_states):
|
||||
"""
|
||||
Create a [`MessageBox`] from a previously known update state.
|
||||
"""
|
||||
if __debug__:
|
||||
self._trace('Loading MessageBox with session_state = %r, channel_states = %r', session_state, channel_states)
|
||||
|
||||
deadline = next_updates_deadline()
|
||||
|
||||
self.map.clear()
|
||||
if session_state.pts != NO_SEQ:
|
||||
self.map[ENTRY_ACCOUNT] = State(pts=session_state.pts, deadline=deadline)
|
||||
if session_state.qts != NO_SEQ:
|
||||
self.map[ENTRY_SECRET] = State(pts=session_state.qts, deadline=deadline)
|
||||
self.map.update((s.channel_id, State(pts=s.pts, deadline=deadline)) for s in channel_states)
|
||||
|
||||
self.date = datetime.datetime.fromtimestamp(session_state.date, tz=datetime.timezone.utc)
|
||||
self.seq = session_state.seq
|
||||
self.next_deadline = ENTRY_ACCOUNT
|
||||
|
||||
def session_state(self):
|
||||
"""
|
||||
Return the current state.
|
||||
|
||||
This should be used for persisting the state.
|
||||
"""
|
||||
return dict(
|
||||
pts=self.map[ENTRY_ACCOUNT].pts if ENTRY_ACCOUNT in self.map else NO_SEQ,
|
||||
qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ,
|
||||
date=self.date,
|
||||
seq=self.seq,
|
||||
), {id: state.pts for id, state in self.map.items() if isinstance(id, int)}
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
"""
|
||||
Return true if the message box is empty and has no state yet.
|
||||
"""
|
||||
return ENTRY_ACCOUNT not in self.map
|
||||
|
||||
def check_deadlines(self):
|
||||
"""
|
||||
Return the next deadline when receiving updates should timeout.
|
||||
|
||||
If a deadline expired, the corresponding entries will be marked as needing to get its difference.
|
||||
While there are entries pending of getting their difference, this method returns the current instant.
|
||||
"""
|
||||
now = get_running_loop().time()
|
||||
|
||||
if self.getting_diff_for:
|
||||
return now
|
||||
|
||||
deadline = next_updates_deadline()
|
||||
|
||||
# Most of the time there will be zero or one gap in flight so finding the minimum is cheap.
|
||||
if self.possible_gaps:
|
||||
deadline = min(deadline, *(gap.deadline for gap in self.possible_gaps.values()))
|
||||
elif self.next_deadline in self.map:
|
||||
deadline = min(deadline, self.map[self.next_deadline].deadline)
|
||||
|
||||
# asyncio's loop time precision only seems to be about 3 decimal places, so it's possible that
|
||||
# we find the same number again on repeated calls. Without the "or equal" part we would log the
|
||||
# timeout for updates several times (it also makes sense to get difference if now is the deadline).
|
||||
if now >= deadline:
|
||||
# Check all expired entries and add them to the list that needs getting difference.
|
||||
self.getting_diff_for.update(entry for entry, gap in self.possible_gaps.items() if now > gap.deadline)
|
||||
self.getting_diff_for.update(entry for entry, state in self.map.items() if now > state.deadline)
|
||||
|
||||
if __debug__:
|
||||
self._trace('Deadlines met, now getting diff for %r', self.getting_diff_for)
|
||||
|
||||
# When extending `getting_diff_for`, it's important to have the moral equivalent of
|
||||
# `begin_get_diff` (that is, clear possible gaps if we're now getting difference).
|
||||
for entry in self.getting_diff_for:
|
||||
self.possible_gaps.pop(entry, None)
|
||||
|
||||
return deadline
|
||||
|
||||
# Reset the deadline for the periods without updates for a given entry.
|
||||
#
|
||||
# It also updates the next deadline time to reflect the new closest deadline.
|
||||
def reset_deadline(self, entry, deadline):
|
||||
if entry not in self.map:
|
||||
raise RuntimeError('Called reset_deadline on an entry for which we do not have state')
|
||||
self.map[entry].deadline = deadline
|
||||
|
||||
if self.next_deadline == entry:
|
||||
# If the updated deadline was the closest one, recalculate the new minimum.
|
||||
self.next_deadline = min(self.map.items(), key=lambda entry_state: entry_state[1].deadline)[0]
|
||||
elif self.next_deadline in self.map and deadline < self.map[self.next_deadline].deadline:
|
||||
# If the updated deadline is smaller than the next deadline, change the next deadline to be the new one.
|
||||
self.next_deadline = entry
|
||||
# else an unrelated deadline was updated, so the closest one remains unchanged.
|
||||
|
||||
# Convenience to reset a channel's deadline, with optional timeout.
|
||||
def reset_channel_deadline(self, channel_id, timeout):
|
||||
self.reset_deadline(channel_id, get_running_loop().time() + (timeout or NO_UPDATES_TIMEOUT))
|
||||
|
||||
# Reset all the deadlines in `reset_deadlines_for` and then empty the set.
|
||||
def apply_deadlines_reset(self):
|
||||
next_deadline = next_updates_deadline()
|
||||
|
||||
reset_deadlines_for = self.reset_deadlines_for
|
||||
self.reset_deadlines_for = set() # "move" the set to avoid self.reset_deadline() from touching it during iter
|
||||
|
||||
for entry in reset_deadlines_for:
|
||||
self.reset_deadline(entry, next_deadline)
|
||||
|
||||
reset_deadlines_for.clear() # reuse allocation, the other empty set was a temporary dummy value
|
||||
self.reset_deadlines_for = reset_deadlines_for
|
||||
|
||||
# Sets the update state.
|
||||
#
|
||||
# Should be called right after login if [`MessageBox::new`] was used, otherwise undesirable
|
||||
# updates will be fetched.
|
||||
def set_state(self, state, reset=True):
|
||||
if __debug__:
|
||||
self._trace('Setting state %s', state)
|
||||
|
||||
deadline = next_updates_deadline()
|
||||
|
||||
if state.pts != NO_SEQ or not reset:
|
||||
self.map[ENTRY_ACCOUNT] = State(pts=state.pts, deadline=deadline)
|
||||
else:
|
||||
self.map.pop(ENTRY_ACCOUNT, None)
|
||||
|
||||
# Telegram seems to use the `qts` for bot accounts, but while applying difference,
|
||||
# it might be reset back to 0. See issue #3873 for more details.
|
||||
#
|
||||
# During login, a value of zero would mean the `pts` is unknown,
|
||||
# so the map shouldn't contain that entry.
|
||||
# But while applying difference, if the value is zero, it (probably)
|
||||
# truly means that's what should be used (hence the `reset` flag).
|
||||
if state.qts != NO_SEQ or not reset:
|
||||
self.map[ENTRY_SECRET] = State(pts=state.qts, deadline=deadline)
|
||||
else:
|
||||
self.map.pop(ENTRY_SECRET, None)
|
||||
|
||||
self.date = state.date
|
||||
self.seq = state.seq
|
||||
|
||||
# Like [`MessageBox::set_state`], but for channels. Useful when getting dialogs.
|
||||
#
|
||||
# The update state will only be updated if no entry was known previously.
|
||||
def try_set_channel_state(self, id, pts):
|
||||
if __debug__:
|
||||
self._trace('Trying to set channel state for %r: %r', id, pts)
|
||||
|
||||
if id not in self.map:
|
||||
self.map[id] = State(pts=pts, deadline=next_updates_deadline())
|
||||
|
||||
# Try to begin getting difference for the given entry.
|
||||
# Fails if the entry does not have a previously-known state that can be used to get its difference.
|
||||
#
|
||||
# Clears any previous gaps.
|
||||
def try_begin_get_diff(self, entry):
|
||||
if entry not in self.map:
|
||||
# Won't actually be able to get difference for this entry if we don't have a pts to start off from.
|
||||
if entry in self.possible_gaps:
|
||||
raise RuntimeError('Should not have a possible_gap for an entry not in the state map')
|
||||
|
||||
# TODO it would be useful to log when this happens
|
||||
return
|
||||
|
||||
self.getting_diff_for.add(entry)
|
||||
self.possible_gaps.pop(entry, None)
|
||||
|
||||
# Finish getting difference for the given entry.
|
||||
#
|
||||
# It also resets the deadline.
|
||||
def end_get_diff(self, entry):
|
||||
try:
|
||||
self.getting_diff_for.remove(entry)
|
||||
except KeyError:
|
||||
raise RuntimeError('Called end_get_diff on an entry which was not getting diff for')
|
||||
|
||||
self.reset_deadline(entry, next_updates_deadline())
|
||||
assert entry not in self.possible_gaps, "gaps shouldn't be created while getting difference"
|
||||
|
||||
# endregion Creation, querying, and setting base state.
|
||||
|
||||
# region "Normal" updates flow (processing and detection of gaps).
|
||||
|
||||
# Process an update and return what should be done with it.
|
||||
#
|
||||
# Updates corresponding to entries for which their difference is currently being fetched
|
||||
# will be ignored. While according to the [updates' documentation]:
|
||||
#
|
||||
# > Implementations [have] to postpone updates received via the socket while
|
||||
# > filling gaps in the event and `Update` sequences, as well as avoid filling
|
||||
# > gaps in the same sequence.
|
||||
#
|
||||
# In practice, these updates should have also been retrieved through getting difference.
|
||||
#
|
||||
# [updates documentation] https://core.telegram.org/api/updates
|
||||
def process_updates(
|
||||
self,
|
||||
updates,
|
||||
chat_hashes,
|
||||
result, # out list of updates; returns list of user, chat, or raise if gap
|
||||
):
|
||||
if __debug__:
|
||||
self._trace('Processing updates %s', updates)
|
||||
|
||||
date = getattr(updates, 'date', None)
|
||||
if date is None:
|
||||
# updatesTooLong is the only one with no date (we treat it as a gap)
|
||||
self.try_begin_get_diff(ENTRY_ACCOUNT)
|
||||
raise GapError
|
||||
|
||||
# v1 has never sent updates produced by the client itself to the handlers.
|
||||
# However proper update handling requires those to be processed.
|
||||
# This is an ugly workaround for that.
|
||||
self_outgoing = getattr(updates, '_self_outgoing', False)
|
||||
real_result = result
|
||||
result = []
|
||||
|
||||
seq = getattr(updates, 'seq', None) or NO_SEQ
|
||||
seq_start = getattr(updates, 'seq_start', None) or seq
|
||||
users = getattr(updates, 'users', None) or []
|
||||
chats = getattr(updates, 'chats', None) or []
|
||||
|
||||
# updateShort is the only update which cannot be dispatched directly but doesn't have 'updates' field
|
||||
updates = getattr(updates, 'updates', None) or [updates.update if isinstance(updates, tl.UpdateShort) else updates]
|
||||
|
||||
for u in updates:
|
||||
u._self_outgoing = self_outgoing
|
||||
|
||||
# > For all the other [not `updates` or `updatesCombined`] `Updates` type constructors
|
||||
# > there is no need to check `seq` or change a local state.
|
||||
if seq_start != NO_SEQ:
|
||||
if self.seq + 1 > seq_start:
|
||||
# Skipping updates that were already handled
|
||||
return (users, chats)
|
||||
elif self.seq + 1 < seq_start:
|
||||
# Gap detected
|
||||
self.try_begin_get_diff(ENTRY_ACCOUNT)
|
||||
raise GapError
|
||||
# else apply
|
||||
|
||||
self.date = date
|
||||
if seq != NO_SEQ:
|
||||
self.seq = seq
|
||||
|
||||
def _sort_gaps(update):
|
||||
pts = PtsInfo.from_update(update)
|
||||
return pts.pts - pts.pts_count if pts else 0
|
||||
|
||||
# Telegram can send updates out of order (e.g. ReadChannelInbox first
|
||||
# and then NewChannelMessage, both with the same pts, but the count is
|
||||
# 0 and 1 respectively).
|
||||
#
|
||||
# We can't know beforehand if this would cause issues (i.e. if any of
|
||||
# the updates is the first one we get to know about a specific channel)
|
||||
# (other than doing a pre-scan to check if any has info about an entry
|
||||
# we lack), so instead we sort preemptively. As a bonus there's less
|
||||
# likelyhood of "possible gaps" by doing this.
|
||||
# TODO give this more thought, perhaps possible gaps can't happen at all
|
||||
# (not ones which would be resolved by sorting anyway)
|
||||
result.extend(filter(None, (
|
||||
self.apply_pts_info(u, reset_deadline=True) for u in sorted(updates, key=_sort_gaps))))
|
||||
|
||||
self.apply_deadlines_reset()
|
||||
|
||||
if self.possible_gaps:
|
||||
# For each update in possible gaps, see if the gap has been resolved already.
|
||||
for key in list(self.possible_gaps.keys()):
|
||||
self.possible_gaps[key].updates.sort(key=_sort_gaps)
|
||||
|
||||
for _ in range(len(self.possible_gaps[key].updates)):
|
||||
update = self.possible_gaps[key].updates.pop(0)
|
||||
|
||||
# If this fails to apply, it will get re-inserted at the end.
|
||||
# All should fail, so the order will be preserved (it would've cycled once).
|
||||
update = self.apply_pts_info(update, reset_deadline=False)
|
||||
if update:
|
||||
result.append(update)
|
||||
|
||||
# Clear now-empty gaps.
|
||||
self.possible_gaps = {entry: gap for entry, gap in self.possible_gaps.items() if gap.updates}
|
||||
|
||||
real_result.extend(u for u in result if not u._self_outgoing)
|
||||
|
||||
return (users, chats)
|
||||
|
||||
# Tries to apply the input update if its `PtsInfo` follows the correct order.
|
||||
#
|
||||
# If the update can be applied, it is returned; otherwise, the update is stored in a
|
||||
# possible gap (unless it was already handled or would be handled through getting
|
||||
# difference) and `None` is returned.
|
||||
def apply_pts_info(
|
||||
self,
|
||||
update,
|
||||
*,
|
||||
reset_deadline,
|
||||
):
|
||||
# This update means we need to call getChannelDifference to get the updates from the channel
|
||||
if isinstance(update, tl.UpdateChannelTooLong):
|
||||
self.try_begin_get_diff(update.channel_id)
|
||||
return None
|
||||
|
||||
pts = PtsInfo.from_update(update)
|
||||
if not pts:
|
||||
# No pts means that the update can be applied in any order.
|
||||
return update
|
||||
|
||||
# As soon as we receive an update of any form related to messages (has `PtsInfo`),
|
||||
# the "no updates" period for that entry is reset.
|
||||
#
|
||||
# Build the `HashSet` to avoid calling `reset_deadline` more than once for the same entry.
|
||||
#
|
||||
# By the time this method returns, self.map will have an entry for which we can reset its deadline.
|
||||
if reset_deadline:
|
||||
self.reset_deadlines_for.add(pts.entry)
|
||||
|
||||
if pts.entry in self.getting_diff_for:
|
||||
# Note: early returning here also prevents gap from being inserted (which they should
|
||||
# not be while getting difference).
|
||||
return None
|
||||
|
||||
if pts.entry in self.map:
|
||||
local_pts = self.map[pts.entry].pts
|
||||
if local_pts + pts.pts_count > pts.pts:
|
||||
# Ignore
|
||||
return None
|
||||
elif local_pts + pts.pts_count < pts.pts:
|
||||
# Possible gap
|
||||
# TODO store chats too?
|
||||
if pts.entry not in self.possible_gaps:
|
||||
self.possible_gaps[pts.entry] = PossibleGap(
|
||||
deadline=get_running_loop().time() + POSSIBLE_GAP_TIMEOUT,
|
||||
updates=[]
|
||||
)
|
||||
|
||||
self.possible_gaps[pts.entry].updates.append(update)
|
||||
return None
|
||||
else:
|
||||
# Apply
|
||||
pass
|
||||
|
||||
# In a channel, we may immediately receive:
|
||||
# * ReadChannelInbox (pts = X, pts_count = 0)
|
||||
# * NewChannelMessage (pts = X, pts_count = 1)
|
||||
#
|
||||
# Notice how both `pts` are the same. If they were to be applied out of order, the first
|
||||
# one however would've triggered a gap because `local_pts` + `pts_count` of 0 would be
|
||||
# less than `remote_pts`. So there is no risk by setting the `local_pts` to match the
|
||||
# `remote_pts` here of missing the new message.
|
||||
#
|
||||
# The message would however be lost if we initialized the pts with the first one, since
|
||||
# the second one would appear "already handled". To prevent this we set the pts to be
|
||||
# one less when the count is 0 (which might be wrong and trigger a gap later on, but is
|
||||
# unlikely). This will prevent us from losing updates in the unlikely scenario where these
|
||||
# two updates arrive in different packets (and therefore couldn't be sorted beforehand).
|
||||
if pts.entry in self.map:
|
||||
self.map[pts.entry].pts = pts.pts
|
||||
else:
|
||||
# When a chat is migrated to a megagroup, the first update can be a `ReadChannelInbox`
|
||||
# with `pts = 1, pts_count = 0` followed by a `NewChannelMessage` with `pts = 2, pts_count=1`.
|
||||
# Note how the `pts` for the message is 2 and not 1 unlike the case described before!
|
||||
# This is likely because the `pts` cannot be 0 (or it would fail with PERSISTENT_TIMESTAMP_EMPTY),
|
||||
# which forces the first update to be 1. But if we got difference with 1 and the second update
|
||||
# also used 1, we would miss it, so Telegram probably uses 2 to work around that.
|
||||
self.map[pts.entry] = State(
|
||||
pts=(pts.pts - (0 if pts.pts_count else 1)) or 1,
|
||||
deadline=next_updates_deadline()
|
||||
)
|
||||
|
||||
return update
|
||||
|
||||
# endregion "Normal" updates flow (processing and detection of gaps).
|
||||
|
||||
# region Getting and applying account difference.
|
||||
|
||||
# Return the request that needs to be made to get the difference, if any.
|
||||
def get_difference(self):
|
||||
for entry in (ENTRY_ACCOUNT, ENTRY_SECRET):
|
||||
if entry in self.getting_diff_for:
|
||||
if entry not in self.map:
|
||||
raise RuntimeError('Should not try to get difference for an entry without known state')
|
||||
|
||||
gd = fn.updates.GetDifferenceRequest(
|
||||
pts=self.map[ENTRY_ACCOUNT].pts,
|
||||
pts_total_limit=None,
|
||||
date=self.date,
|
||||
qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ,
|
||||
)
|
||||
if __debug__:
|
||||
self._trace('Requesting account difference %s', gd)
|
||||
return gd
|
||||
|
||||
return None
|
||||
|
||||
# Similar to [`MessageBox::process_updates`], but using the result from getting difference.
|
||||
def apply_difference(
|
||||
self,
|
||||
diff,
|
||||
chat_hashes,
|
||||
):
|
||||
if __debug__:
|
||||
self._trace('Applying account difference %s', diff)
|
||||
|
||||
finish = None
|
||||
result = None
|
||||
|
||||
if isinstance(diff, tl.updates.DifferenceEmpty):
|
||||
finish = True
|
||||
self.date = diff.date
|
||||
self.seq = diff.seq
|
||||
result = [], [], []
|
||||
elif isinstance(diff, tl.updates.Difference):
|
||||
finish = True
|
||||
chat_hashes.extend(diff.users, diff.chats)
|
||||
result = self.apply_difference_type(diff, chat_hashes)
|
||||
elif isinstance(diff, tl.updates.DifferenceSlice):
|
||||
finish = False
|
||||
chat_hashes.extend(diff.users, diff.chats)
|
||||
result = self.apply_difference_type(diff, chat_hashes)
|
||||
elif isinstance(diff, tl.updates.DifferenceTooLong):
|
||||
finish = True
|
||||
self.map[ENTRY_ACCOUNT].pts = diff.pts # the deadline will be reset once the diff ends
|
||||
result = [], [], []
|
||||
|
||||
if finish:
|
||||
account = ENTRY_ACCOUNT in self.getting_diff_for
|
||||
secret = ENTRY_SECRET in self.getting_diff_for
|
||||
|
||||
if not account and not secret:
|
||||
raise RuntimeError('Should not be applying the difference when neither account or secret was diff was active')
|
||||
|
||||
# Both may be active if both expired at the same time.
|
||||
if account:
|
||||
self.end_get_diff(ENTRY_ACCOUNT)
|
||||
if secret:
|
||||
self.end_get_diff(ENTRY_SECRET)
|
||||
|
||||
return result
|
||||
|
||||
def apply_difference_type(
|
||||
self,
|
||||
diff,
|
||||
chat_hashes,
|
||||
):
|
||||
state = getattr(diff, 'intermediate_state', None) or diff.state
|
||||
self.set_state(state, reset=False)
|
||||
|
||||
# diff.other_updates can contain things like UpdateChannelTooLong and UpdateNewChannelMessage.
|
||||
# We need to process those as if they were socket updates to discard any we have already handled.
|
||||
updates = []
|
||||
self.process_updates(tl.Updates(
|
||||
updates=diff.other_updates,
|
||||
users=diff.users,
|
||||
chats=diff.chats,
|
||||
date=1, # anything not-None
|
||||
seq=NO_SEQ, # this way date is not used
|
||||
), chat_hashes, updates)
|
||||
|
||||
updates.extend(tl.UpdateNewMessage(
|
||||
message=m,
|
||||
pts=NO_SEQ,
|
||||
pts_count=NO_SEQ,
|
||||
) for m in diff.new_messages)
|
||||
updates.extend(tl.UpdateNewEncryptedMessage(
|
||||
message=m,
|
||||
qts=NO_SEQ,
|
||||
) for m in diff.new_encrypted_messages)
|
||||
|
||||
return updates, diff.users, diff.chats
|
||||
|
||||
def end_difference(self):
|
||||
if __debug__:
|
||||
self._trace('Ending account difference')
|
||||
|
||||
account = ENTRY_ACCOUNT in self.getting_diff_for
|
||||
secret = ENTRY_SECRET in self.getting_diff_for
|
||||
|
||||
if not account and not secret:
|
||||
raise RuntimeError('Should not be ending get difference when neither account or secret was diff was active')
|
||||
|
||||
# Both may be active if both expired at the same time.
|
||||
if account:
|
||||
self.end_get_diff(ENTRY_ACCOUNT)
|
||||
if secret:
|
||||
self.end_get_diff(ENTRY_SECRET)
|
||||
|
||||
# endregion Getting and applying account difference.
|
||||
|
||||
# region Getting and applying channel difference.
|
||||
|
||||
# Return the request that needs to be made to get a channel's difference, if any.
|
||||
def get_channel_difference(
|
||||
self,
|
||||
chat_hashes,
|
||||
):
|
||||
entry = next((id for id in self.getting_diff_for if isinstance(id, int)), None)
|
||||
if not entry:
|
||||
return None
|
||||
|
||||
packed = chat_hashes.get(entry)
|
||||
if not packed:
|
||||
# Cannot get channel difference as we're missing its hash
|
||||
# TODO we should probably log this
|
||||
self.end_get_diff(entry)
|
||||
# Remove the outdated `pts` entry from the map so that the next update can correct
|
||||
# it. Otherwise, it will spam that the access hash is missing.
|
||||
self.map.pop(entry, None)
|
||||
return None
|
||||
|
||||
state = self.map.get(entry)
|
||||
if not state:
|
||||
raise RuntimeError('Should not try to get difference for an entry without known state')
|
||||
|
||||
gd = fn.updates.GetChannelDifferenceRequest(
|
||||
force=False,
|
||||
channel=tl.InputChannel(packed.id, packed.hash),
|
||||
filter=tl.ChannelMessagesFilterEmpty(),
|
||||
pts=state.pts,
|
||||
limit=BOT_CHANNEL_DIFF_LIMIT if chat_hashes.self_bot else USER_CHANNEL_DIFF_LIMIT
|
||||
)
|
||||
if __debug__:
|
||||
self._trace('Requesting channel difference %s', gd)
|
||||
return gd
|
||||
|
||||
# Similar to [`MessageBox::process_updates`], but using the result from getting difference.
|
||||
def apply_channel_difference(
|
||||
self,
|
||||
request,
|
||||
diff,
|
||||
chat_hashes,
|
||||
):
|
||||
entry = request.channel.channel_id
|
||||
if __debug__:
|
||||
self._trace('Applying channel difference for %r: %s', entry, diff)
|
||||
|
||||
self.possible_gaps.pop(entry, None)
|
||||
|
||||
if isinstance(diff, tl.updates.ChannelDifferenceEmpty):
|
||||
assert diff.final
|
||||
self.end_get_diff(entry)
|
||||
self.map[entry].pts = diff.pts
|
||||
return [], [], []
|
||||
elif isinstance(diff, tl.updates.ChannelDifferenceTooLong):
|
||||
assert diff.final
|
||||
self.map[entry].pts = diff.dialog.pts
|
||||
chat_hashes.extend(diff.users, diff.chats)
|
||||
self.reset_channel_deadline(entry, diff.timeout)
|
||||
# This `diff` has the "latest messages and corresponding chats", but it would
|
||||
# be strange to give the user only partial changes of these when they would
|
||||
# expect all updates to be fetched. Instead, nothing is returned.
|
||||
return [], [], []
|
||||
elif isinstance(diff, tl.updates.ChannelDifference):
|
||||
if diff.final:
|
||||
self.end_get_diff(entry)
|
||||
|
||||
self.map[entry].pts = diff.pts
|
||||
chat_hashes.extend(diff.users, diff.chats)
|
||||
|
||||
updates = []
|
||||
self.process_updates(tl.Updates(
|
||||
updates=diff.other_updates,
|
||||
users=diff.users,
|
||||
chats=diff.chats,
|
||||
date=1, # anything not-None
|
||||
seq=NO_SEQ, # this way date is not used
|
||||
), chat_hashes, updates)
|
||||
|
||||
updates.extend(tl.UpdateNewChannelMessage(
|
||||
message=m,
|
||||
pts=NO_SEQ,
|
||||
pts_count=NO_SEQ,
|
||||
) for m in diff.new_messages)
|
||||
self.reset_channel_deadline(entry, None)
|
||||
|
||||
return updates, diff.users, diff.chats
|
||||
|
||||
def end_channel_difference(self, request, reason: PrematureEndReason, chat_hashes):
|
||||
entry = request.channel.channel_id
|
||||
if __debug__:
|
||||
self._trace('Ending channel difference for %r because %s', entry, reason)
|
||||
|
||||
if reason == PrematureEndReason.TEMPORARY_SERVER_ISSUES:
|
||||
# Temporary issues. End getting difference without updating the pts so we can retry later.
|
||||
self.possible_gaps.pop(entry, None)
|
||||
self.end_get_diff(entry)
|
||||
elif reason == PrematureEndReason.BANNED:
|
||||
# Banned in the channel. Forget its state since we can no longer fetch updates from it.
|
||||
self.possible_gaps.pop(entry, None)
|
||||
self.end_get_diff(entry)
|
||||
del self.map[entry]
|
||||
else:
|
||||
raise RuntimeError('Unknown reason to end channel difference')
|
||||
|
||||
# endregion Getting and applying channel difference.
|
|
@ -1,195 +0,0 @@
|
|||
from typing import Optional, Tuple
|
||||
from enum import IntEnum
|
||||
from ..tl.types import InputPeerUser, InputPeerChat, InputPeerChannel
|
||||
|
||||
|
||||
class SessionState:
|
||||
"""
|
||||
Stores the information needed to fetch updates and about the current user.
|
||||
|
||||
* user_id: 64-bit number representing the user identifier.
|
||||
* dc_id: 32-bit number relating to the datacenter identifier where the user is.
|
||||
* bot: is the logged-in user a bot?
|
||||
* pts: 64-bit number holding the state needed to fetch updates.
|
||||
* qts: alternative 64-bit number holding the state needed to fetch updates.
|
||||
* date: 64-bit number holding the date needed to fetch updates.
|
||||
* seq: 64-bit-number holding the sequence number needed to fetch updates.
|
||||
* takeout_id: 64-bit-number holding the identifier of the current takeout session.
|
||||
|
||||
Note that some of the numbers will only use 32 out of the 64 available bits.
|
||||
However, for future-proofing reasons, we recommend you pretend they are 64-bit long.
|
||||
"""
|
||||
__slots__ = ('user_id', 'dc_id', 'bot', 'pts', 'qts', 'date', 'seq', 'takeout_id')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: int,
|
||||
dc_id: int,
|
||||
bot: bool,
|
||||
pts: int,
|
||||
qts: int,
|
||||
date: int,
|
||||
seq: int,
|
||||
takeout_id: Optional[int]
|
||||
):
|
||||
self.user_id = user_id
|
||||
self.dc_id = dc_id
|
||||
self.bot = bot
|
||||
self.pts = pts
|
||||
self.qts = qts
|
||||
self.date = date
|
||||
self.seq = seq
|
||||
self.takeout_id = takeout_id
|
||||
|
||||
def __repr__(self):
|
||||
return repr({k: getattr(self, k) for k in self.__slots__})
|
||||
|
||||
|
||||
class ChannelState:
|
||||
"""
|
||||
Stores the information needed to fetch updates from a channel.
|
||||
|
||||
* channel_id: 64-bit number representing the channel identifier.
|
||||
* pts: 64-bit number holding the state needed to fetch updates.
|
||||
"""
|
||||
__slots__ = ('channel_id', 'pts')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
channel_id: int,
|
||||
pts: int,
|
||||
):
|
||||
self.channel_id = channel_id
|
||||
self.pts = pts
|
||||
|
||||
def __repr__(self):
|
||||
return repr({k: getattr(self, k) for k in self.__slots__})
|
||||
|
||||
|
||||
class EntityType(IntEnum):
|
||||
"""
|
||||
You can rely on the type value to be equal to the ASCII character one of:
|
||||
|
||||
* 'U' (85): this entity belongs to a :tl:`User` who is not a ``bot``.
|
||||
* 'B' (66): this entity belongs to a :tl:`User` who is a ``bot``.
|
||||
* 'G' (71): this entity belongs to a small group :tl:`Chat`.
|
||||
* 'C' (67): this entity belongs to a standard broadcast :tl:`Channel`.
|
||||
* 'M' (77): this entity belongs to a megagroup :tl:`Channel`.
|
||||
* 'E' (69): this entity belongs to an "enormous" "gigagroup" :tl:`Channel`.
|
||||
"""
|
||||
USER = ord('U')
|
||||
BOT = ord('B')
|
||||
GROUP = ord('G')
|
||||
CHANNEL = ord('C')
|
||||
MEGAGROUP = ord('M')
|
||||
GIGAGROUP = ord('E')
|
||||
|
||||
def canonical(self):
|
||||
"""
|
||||
Return the canonical version of this type.
|
||||
"""
|
||||
return _canon_entity_types[self]
|
||||
|
||||
|
||||
_canon_entity_types = {
|
||||
EntityType.USER: EntityType.USER,
|
||||
EntityType.BOT: EntityType.USER,
|
||||
EntityType.GROUP: EntityType.GROUP,
|
||||
EntityType.CHANNEL: EntityType.CHANNEL,
|
||||
EntityType.MEGAGROUP: EntityType.CHANNEL,
|
||||
EntityType.GIGAGROUP: EntityType.CHANNEL,
|
||||
}
|
||||
|
||||
|
||||
class Entity:
|
||||
"""
|
||||
Stores the information needed to use a certain user, chat or channel with the API.
|
||||
|
||||
* ty: 8-bit number indicating the type of the entity (of type `EntityType`).
|
||||
* id: 64-bit number uniquely identifying the entity among those of the same type.
|
||||
* hash: 64-bit signed number needed to use this entity with the API.
|
||||
|
||||
The string representation of this class is considered to be stable, for as long as
|
||||
Telegram doesn't need to add more fields to the entities. It can also be converted
|
||||
to bytes with ``bytes(entity)``, for a more compact representation.
|
||||
"""
|
||||
__slots__ = ('ty', 'id', 'hash')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ty: EntityType,
|
||||
id: int,
|
||||
hash: int
|
||||
):
|
||||
self.ty = ty
|
||||
self.id = id
|
||||
self.hash = hash
|
||||
|
||||
@property
|
||||
def is_user(self):
|
||||
"""
|
||||
``True`` if the entity is either a user or a bot.
|
||||
"""
|
||||
return self.ty in (EntityType.USER, EntityType.BOT)
|
||||
|
||||
@property
|
||||
def is_group(self):
|
||||
"""
|
||||
``True`` if the entity is a small group chat or `megagroup`_.
|
||||
|
||||
.. _megagroup: https://telegram.org/blog/supergroups5k
|
||||
"""
|
||||
return self.ty in (EntityType.GROUP, EntityType.MEGAGROUP)
|
||||
|
||||
@property
|
||||
def is_broadcast(self):
|
||||
"""
|
||||
``True`` if the entity is a broadcast channel or `broadcast group`_.
|
||||
|
||||
.. _broadcast group: https://telegram.org/blog/autodelete-inv2#groups-with-unlimited-members
|
||||
"""
|
||||
return self.ty in (EntityType.CHANNEL, EntityType.GIGAGROUP)
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, string: str):
|
||||
"""
|
||||
Convert the string into an `Entity`.
|
||||
"""
|
||||
try:
|
||||
ty, id, hash = string.split('.')
|
||||
ty, id, hash = ord(ty), int(id), int(hash)
|
||||
except AttributeError:
|
||||
raise TypeError(f'expected str, got {string!r}') from None
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError(f'malformed entity str (must be T.id.hash), got {string!r}') from None
|
||||
|
||||
return cls(EntityType(ty), id, hash)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, blob):
|
||||
"""
|
||||
Convert the bytes into an `Entity`.
|
||||
"""
|
||||
try:
|
||||
ty, id, hash = struct.unpack('<Bqq', blob)
|
||||
except struct.error:
|
||||
raise ValueError(f'malformed entity data, got {string!r}') from None
|
||||
|
||||
return cls(EntityType(ty), id, hash)
|
||||
|
||||
def __str__(self):
|
||||
return f'{chr(self.ty)}.{self.id}.{self.hash}'
|
||||
|
||||
def __bytes__(self):
|
||||
return struct.pack('<Bqq', self.ty, self.id, self.hash)
|
||||
|
||||
def _as_input_peer(self):
|
||||
if self.is_user:
|
||||
return InputPeerUser(self.id, self.hash)
|
||||
elif self.ty == EntityType.GROUP:
|
||||
return InputPeerChat(self.id)
|
||||
else:
|
||||
return InputPeerChannel(self.id, self.hash)
|
||||
|
||||
def __repr__(self):
|
||||
return repr({k: getattr(self, k) for k in self.__slots__})
|
|
@ -1,25 +0,0 @@
|
|||
"""
|
||||
This package defines clients as subclasses of others, and then a single
|
||||
`telethon.client.telegramclient.TelegramClient` which is subclass of them
|
||||
all to provide the final unified interface while the methods can live in
|
||||
different subclasses to be more maintainable.
|
||||
|
||||
The ABC is `telethon.client.telegrambaseclient.TelegramBaseClient` and the
|
||||
first implementor is `telethon.client.users.UserMethods`, since calling
|
||||
requests require them to be resolved first, and that requires accessing
|
||||
entities (users).
|
||||
"""
|
||||
from .telegrambaseclient import TelegramBaseClient
|
||||
from .users import UserMethods # Required for everything
|
||||
from .messageparse import MessageParseMethods # Required for messages
|
||||
from .uploads import UploadMethods # Required for messages to send files
|
||||
from .updates import UpdateMethods # Required for buttons (register callbacks)
|
||||
from .buttons import ButtonMethods # Required for messages to use buttons
|
||||
from .messages import MessageMethods
|
||||
from .chats import ChatMethods
|
||||
from .dialogs import DialogMethods
|
||||
from .downloads import DownloadMethods
|
||||
from .account import AccountMethods
|
||||
from .auth import AuthMethods
|
||||
from .bots import BotMethods
|
||||
from .telegramclient import TelegramClient
|
|
@ -1,243 +0,0 @@
|
|||
import functools
|
||||
import inspect
|
||||
import typing
|
||||
|
||||
from .users import _NOT_A_REQUEST
|
||||
from .. import helpers, utils
|
||||
from ..tl import functions, TLRequest
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
||||
|
||||
# TODO Make use of :tl:`InvokeWithMessagesRange` somehow
|
||||
# For that, we need to use :tl:`GetSplitRanges` first.
|
||||
class _TakeoutClient:
|
||||
"""
|
||||
Proxy object over the client.
|
||||
"""
|
||||
__PROXY_INTERFACE = ('__enter__', '__exit__', '__aenter__', '__aexit__')
|
||||
|
||||
def __init__(self, finalize, client, request):
|
||||
# We use the name mangling for attributes to make them inaccessible
|
||||
# from within the shadowed client object and to distinguish them from
|
||||
# its own attributes where needed.
|
||||
self.__finalize = finalize
|
||||
self.__client = client
|
||||
self.__request = request
|
||||
self.__success = None
|
||||
|
||||
@property
|
||||
def success(self):
|
||||
return self.__success
|
||||
|
||||
@success.setter
|
||||
def success(self, value):
|
||||
self.__success = value
|
||||
|
||||
async def __aenter__(self):
|
||||
# Enter/Exit behaviour is "overrode", we don't want to call start.
|
||||
client = self.__client
|
||||
if client.session.takeout_id is None:
|
||||
client.session.takeout_id = (await client(self.__request)).id
|
||||
elif self.__request is not None:
|
||||
raise ValueError("Can't send a takeout request while another "
|
||||
"takeout for the current session still not been finished yet.")
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||
if self.__success is None and self.__finalize:
|
||||
self.__success = exc_type is None
|
||||
|
||||
if self.__success is not None:
|
||||
result = await self(functions.account.FinishTakeoutSessionRequest(
|
||||
self.__success))
|
||||
if not result:
|
||||
raise ValueError("Failed to finish the takeout.")
|
||||
self.session.takeout_id = None
|
||||
|
||||
__enter__ = helpers._sync_enter
|
||||
__exit__ = helpers._sync_exit
|
||||
|
||||
async def __call__(self, request, ordered=False):
|
||||
takeout_id = self.__client.session.takeout_id
|
||||
if takeout_id is None:
|
||||
raise ValueError('Takeout mode has not been initialized '
|
||||
'(are you calling outside of "with"?)')
|
||||
|
||||
single = not utils.is_list_like(request)
|
||||
requests = ((request,) if single else request)
|
||||
wrapped = []
|
||||
for r in requests:
|
||||
if not isinstance(r, TLRequest):
|
||||
raise _NOT_A_REQUEST()
|
||||
await r.resolve(self, utils)
|
||||
wrapped.append(functions.InvokeWithTakeoutRequest(takeout_id, r))
|
||||
|
||||
return await self.__client(
|
||||
wrapped[0] if single else wrapped, ordered=ordered)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
# We access class via type() because __class__ will recurse infinitely.
|
||||
# Also note that since we've name-mangled our own class attributes,
|
||||
# they'll be passed to __getattribute__() as already decorated. For
|
||||
# example, 'self.__client' will be passed as '_TakeoutClient__client'.
|
||||
# https://docs.python.org/3/tutorial/classes.html#private-variables
|
||||
if name.startswith('__') and name not in type(self).__PROXY_INTERFACE:
|
||||
raise AttributeError # force call of __getattr__
|
||||
|
||||
# Try to access attribute in the proxy object and check for the same
|
||||
# attribute in the shadowed object (through our __getattr__) if failed.
|
||||
return super().__getattribute__(name)
|
||||
|
||||
def __getattr__(self, name):
|
||||
value = getattr(self.__client, name)
|
||||
if inspect.ismethod(value):
|
||||
# Emulate bound methods behavior by partially applying our proxy
|
||||
# class as the self parameter instead of the client.
|
||||
return functools.partial(
|
||||
getattr(self.__client.__class__, name), self)
|
||||
|
||||
return value
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name.startswith('_{}__'.format(type(self).__name__.lstrip('_'))):
|
||||
# This is our own name-mangled attribute, keep calm.
|
||||
return super().__setattr__(name, value)
|
||||
return setattr(self.__client, name, value)
|
||||
|
||||
|
||||
class AccountMethods:
|
||||
def takeout(
|
||||
self: 'TelegramClient',
|
||||
finalize: bool = True,
|
||||
*,
|
||||
contacts: bool = None,
|
||||
users: bool = None,
|
||||
chats: bool = None,
|
||||
megagroups: bool = None,
|
||||
channels: bool = None,
|
||||
files: bool = None,
|
||||
max_file_size: bool = None) -> 'TelegramClient':
|
||||
"""
|
||||
Returns a :ref:`telethon-client` which calls methods behind a takeout session.
|
||||
|
||||
It does so by creating a proxy object over the current client through
|
||||
which making requests will use :tl:`InvokeWithTakeoutRequest` to wrap
|
||||
them. In other words, returns the current client modified so that
|
||||
requests are done as a takeout:
|
||||
|
||||
Some of the calls made through the takeout session will have lower
|
||||
flood limits. This is useful if you want to export the data from
|
||||
conversations or mass-download media, since the rate limits will
|
||||
be lower. Only some requests will be affected, and you will need
|
||||
to adjust the `wait_time` of methods like `client.iter_messages
|
||||
<telethon.client.messages.MessageMethods.iter_messages>`.
|
||||
|
||||
By default, all parameters are `None`, and you need to enable those
|
||||
you plan to use by setting them to either `True` or `False`.
|
||||
|
||||
You should ``except errors.TakeoutInitDelayError as e``, since this
|
||||
exception will raise depending on the condition of the session. You
|
||||
can then access ``e.seconds`` to know how long you should wait for
|
||||
before calling the method again.
|
||||
|
||||
There's also a `success` property available in the takeout proxy
|
||||
object, so from the `with` body you can set the boolean result that
|
||||
will be sent back to Telegram. But if it's left `None` as by
|
||||
default, then the action is based on the `finalize` parameter. If
|
||||
it's `True` then the takeout will be finished, and if no exception
|
||||
occurred during it, then `True` will be considered as a result.
|
||||
Otherwise, the takeout will not be finished and its ID will be
|
||||
preserved for future usage as `client.session.takeout_id
|
||||
<telethon.sessions.abstract.Session.takeout_id>`.
|
||||
|
||||
Arguments
|
||||
finalize (`bool`):
|
||||
Whether the takeout session should be finalized upon
|
||||
exit or not.
|
||||
|
||||
contacts (`bool`):
|
||||
Set to `True` if you plan on downloading contacts.
|
||||
|
||||
users (`bool`):
|
||||
Set to `True` if you plan on downloading information
|
||||
from users and their private conversations with you.
|
||||
|
||||
chats (`bool`):
|
||||
Set to `True` if you plan on downloading information
|
||||
from small group chats, such as messages and media.
|
||||
|
||||
megagroups (`bool`):
|
||||
Set to `True` if you plan on downloading information
|
||||
from megagroups (channels), such as messages and media.
|
||||
|
||||
channels (`bool`):
|
||||
Set to `True` if you plan on downloading information
|
||||
from broadcast channels, such as messages and media.
|
||||
|
||||
files (`bool`):
|
||||
Set to `True` if you plan on downloading media and
|
||||
you don't only wish to export messages.
|
||||
|
||||
max_file_size (`int`):
|
||||
The maximum file size, in bytes, that you plan
|
||||
to download for each message with media.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import errors
|
||||
|
||||
try:
|
||||
async with client.takeout() as takeout:
|
||||
await client.get_messages('me') # normal call
|
||||
await takeout.get_messages('me') # wrapped through takeout (less limits)
|
||||
|
||||
async for message in takeout.iter_messages(chat, wait_time=0):
|
||||
... # Do something with the message
|
||||
|
||||
except errors.TakeoutInitDelayError as e:
|
||||
print('Must wait', e.seconds, 'before takeout')
|
||||
"""
|
||||
request_kwargs = dict(
|
||||
contacts=contacts,
|
||||
message_users=users,
|
||||
message_chats=chats,
|
||||
message_megagroups=megagroups,
|
||||
message_channels=channels,
|
||||
files=files,
|
||||
file_max_size=max_file_size
|
||||
)
|
||||
arg_specified = (arg is not None for arg in request_kwargs.values())
|
||||
|
||||
if self.session.takeout_id is None or any(arg_specified):
|
||||
request = functions.account.InitTakeoutSessionRequest(
|
||||
**request_kwargs)
|
||||
else:
|
||||
request = None
|
||||
|
||||
return _TakeoutClient(finalize, self, request)
|
||||
|
||||
async def end_takeout(self: 'TelegramClient', success: bool) -> bool:
|
||||
"""
|
||||
Finishes the current takeout session.
|
||||
|
||||
Arguments
|
||||
success (`bool`):
|
||||
Whether the takeout completed successfully or not.
|
||||
|
||||
Returns
|
||||
`True` if the operation was successful, `False` otherwise.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
await client.end_takeout(success=False)
|
||||
"""
|
||||
try:
|
||||
async with _TakeoutClient(True, self, None) as takeout:
|
||||
takeout.success = success
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
|
@ -1,665 +0,0 @@
|
|||
import getpass
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
from .. import utils, helpers, errors, password as pwd_mod
|
||||
from ..tl import types, functions, custom
|
||||
from .._updates import SessionState
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
||||
|
||||
class AuthMethods:
|
||||
|
||||
# region Public methods
|
||||
|
||||
def start(
|
||||
self: 'TelegramClient',
|
||||
phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '),
|
||||
password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '),
|
||||
*,
|
||||
bot_token: str = None,
|
||||
force_sms: bool = False,
|
||||
code_callback: typing.Callable[[], typing.Union[str, int]] = None,
|
||||
first_name: str = 'New User',
|
||||
last_name: str = '',
|
||||
max_attempts: int = 3) -> 'TelegramClient':
|
||||
"""
|
||||
Starts the client (connects and logs in if necessary).
|
||||
|
||||
By default, this method will be interactive (asking for
|
||||
user input if needed), and will handle 2FA if enabled too.
|
||||
|
||||
If the event loop is already running, this method returns a
|
||||
coroutine that you should await on your own code; otherwise
|
||||
the loop is ran until said coroutine completes.
|
||||
|
||||
Arguments
|
||||
phone (`str` | `int` | `callable`):
|
||||
The phone (or callable without arguments to get it)
|
||||
to which the code will be sent. If a bot-token-like
|
||||
string is given, it will be used as such instead.
|
||||
The argument may be a coroutine.
|
||||
|
||||
password (`str`, `callable`, optional):
|
||||
The password for 2 Factor Authentication (2FA).
|
||||
This is only required if it is enabled in your account.
|
||||
The argument may be a coroutine.
|
||||
|
||||
bot_token (`str`):
|
||||
Bot Token obtained by `@BotFather <https://t.me/BotFather>`_
|
||||
to log in as a bot. Cannot be specified with ``phone`` (only
|
||||
one of either allowed).
|
||||
|
||||
force_sms (`bool`, optional):
|
||||
Whether to force sending the code request as SMS.
|
||||
This only makes sense when signing in with a `phone`.
|
||||
|
||||
code_callback (`callable`, optional):
|
||||
A callable that will be used to retrieve the Telegram
|
||||
login code. Defaults to `input()`.
|
||||
The argument may be a coroutine.
|
||||
|
||||
first_name (`str`, optional):
|
||||
The first name to be used if signing up. This has no
|
||||
effect if the account already exists and you sign in.
|
||||
|
||||
last_name (`str`, optional):
|
||||
Similar to the first name, but for the last. Optional.
|
||||
|
||||
max_attempts (`int`, optional):
|
||||
How many times the code/password callback should be
|
||||
retried or switching between signing in and signing up.
|
||||
|
||||
Returns
|
||||
This `TelegramClient`, so initialization
|
||||
can be chained with ``.start()``.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
client = TelegramClient('anon', api_id, api_hash)
|
||||
|
||||
# Starting as a bot account
|
||||
await client.start(bot_token=bot_token)
|
||||
|
||||
# Starting as a user account
|
||||
await client.start(phone)
|
||||
# Please enter the code you received: 12345
|
||||
# Please enter your password: *******
|
||||
# (You are now logged in)
|
||||
|
||||
# Starting using a context manager (this calls start()):
|
||||
with client:
|
||||
pass
|
||||
"""
|
||||
if code_callback is None:
|
||||
def code_callback():
|
||||
return input('Please enter the code you received: ')
|
||||
elif not callable(code_callback):
|
||||
raise ValueError(
|
||||
'The code_callback parameter needs to be a callable '
|
||||
'function that returns the code you received by Telegram.'
|
||||
)
|
||||
|
||||
if not phone and not bot_token:
|
||||
raise ValueError('No phone number or bot token provided.')
|
||||
|
||||
if phone and bot_token and not callable(phone):
|
||||
raise ValueError('Both a phone and a bot token provided, '
|
||||
'must only provide one of either')
|
||||
|
||||
coro = self._start(
|
||||
phone=phone,
|
||||
password=password,
|
||||
bot_token=bot_token,
|
||||
force_sms=force_sms,
|
||||
code_callback=code_callback,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
max_attempts=max_attempts
|
||||
)
|
||||
return (
|
||||
coro if self.loop.is_running()
|
||||
else self.loop.run_until_complete(coro)
|
||||
)
|
||||
|
||||
async def _start(
|
||||
self: 'TelegramClient', phone, password, bot_token, force_sms,
|
||||
code_callback, first_name, last_name, max_attempts):
|
||||
if not self.is_connected():
|
||||
await self.connect()
|
||||
|
||||
# Rather than using `is_user_authorized`, use `get_me`. While this is
|
||||
# more expensive and needs to retrieve more data from the server, it
|
||||
# enables the library to warn users trying to login to a different
|
||||
# account. See #1172.
|
||||
me = await self.get_me()
|
||||
if me is not None:
|
||||
# The warnings here are on a best-effort and may fail.
|
||||
if bot_token:
|
||||
# bot_token's first part has the bot ID, but it may be invalid
|
||||
# so don't try to parse as int (instead cast our ID to string).
|
||||
if bot_token[:bot_token.find(':')] != str(me.id):
|
||||
warnings.warn(
|
||||
'the session already had an authorized user so it did '
|
||||
'not login to the bot account using the provided '
|
||||
'bot_token (it may not be using the user you expect)'
|
||||
)
|
||||
elif phone and not callable(phone) and utils.parse_phone(phone) != me.phone:
|
||||
warnings.warn(
|
||||
'the session already had an authorized user so it did '
|
||||
'not login to the user account using the provided '
|
||||
'phone (it may not be using the user you expect)'
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
if not bot_token:
|
||||
# Turn the callable into a valid phone number (or bot token)
|
||||
while callable(phone):
|
||||
value = phone()
|
||||
if inspect.isawaitable(value):
|
||||
value = await value
|
||||
|
||||
if ':' in value:
|
||||
# Bot tokens have 'user_id:access_hash' format
|
||||
bot_token = value
|
||||
break
|
||||
|
||||
phone = utils.parse_phone(value) or phone
|
||||
|
||||
if bot_token:
|
||||
await self.sign_in(bot_token=bot_token)
|
||||
return self
|
||||
|
||||
me = None
|
||||
attempts = 0
|
||||
two_step_detected = False
|
||||
|
||||
await self.send_code_request(phone, force_sms=force_sms)
|
||||
while attempts < max_attempts:
|
||||
try:
|
||||
value = code_callback()
|
||||
if inspect.isawaitable(value):
|
||||
value = await value
|
||||
|
||||
# Since sign-in with no code works (it sends the code)
|
||||
# we must double-check that here. Else we'll assume we
|
||||
# logged in, and it will return None as the User.
|
||||
if not value:
|
||||
raise errors.PhoneCodeEmptyError(request=None)
|
||||
|
||||
# Raises SessionPasswordNeededError if 2FA enabled
|
||||
me = await self.sign_in(phone, code=value)
|
||||
break
|
||||
except errors.SessionPasswordNeededError:
|
||||
two_step_detected = True
|
||||
break
|
||||
except (errors.PhoneCodeEmptyError,
|
||||
errors.PhoneCodeExpiredError,
|
||||
errors.PhoneCodeHashEmptyError,
|
||||
errors.PhoneCodeInvalidError):
|
||||
print('Invalid code. Please try again.', file=sys.stderr)
|
||||
|
||||
attempts += 1
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'{} consecutive sign-in attempts failed. Aborting'
|
||||
.format(max_attempts)
|
||||
)
|
||||
|
||||
if two_step_detected:
|
||||
if not password:
|
||||
raise ValueError(
|
||||
"Two-step verification is enabled for this account. "
|
||||
"Please provide the 'password' argument to 'start()'."
|
||||
)
|
||||
|
||||
if callable(password):
|
||||
for _ in range(max_attempts):
|
||||
try:
|
||||
value = password()
|
||||
if inspect.isawaitable(value):
|
||||
value = await value
|
||||
|
||||
me = await self.sign_in(phone=phone, password=value)
|
||||
break
|
||||
except errors.PasswordHashInvalidError:
|
||||
print('Invalid password. Please try again',
|
||||
file=sys.stderr)
|
||||
else:
|
||||
raise errors.PasswordHashInvalidError(request=None)
|
||||
else:
|
||||
me = await 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)
|
||||
tos = '; remember to not break the ToS or you will risk an account ban!'
|
||||
try:
|
||||
print(signed, name, tos, sep='')
|
||||
except UnicodeEncodeError:
|
||||
# Some terminals don't support certain characters
|
||||
print(signed, name.encode('utf-8', errors='ignore')
|
||||
.decode('ascii', errors='ignore'), tos, sep='')
|
||||
|
||||
return self
|
||||
|
||||
def _parse_phone_and_hash(self, phone, phone_hash):
|
||||
"""
|
||||
Helper method to both parse and validate phone and its hash.
|
||||
"""
|
||||
phone = utils.parse_phone(phone) or self._phone
|
||||
if not phone:
|
||||
raise ValueError(
|
||||
'Please make sure to call send_code_request first.'
|
||||
)
|
||||
|
||||
phone_hash = phone_hash or self._phone_code_hash.get(phone, None)
|
||||
if not phone_hash:
|
||||
raise ValueError('You also need to provide a phone_code_hash.')
|
||||
|
||||
return phone, phone_hash
|
||||
|
||||
async def sign_in(
|
||||
self: 'TelegramClient',
|
||||
phone: str = None,
|
||||
code: typing.Union[str, int] = None,
|
||||
*,
|
||||
password: str = None,
|
||||
bot_token: str = None,
|
||||
phone_code_hash: str = None) -> 'typing.Union[types.User, types.auth.SentCode]':
|
||||
"""
|
||||
Logs in to Telegram to an existing user or bot account.
|
||||
|
||||
You should only use this if you are not authorized yet.
|
||||
|
||||
This method will send the code if it's not provided.
|
||||
|
||||
.. note::
|
||||
|
||||
In most cases, you should simply use `start()` and not this method.
|
||||
|
||||
Arguments
|
||||
phone (`str` | `int`):
|
||||
The phone to send the code to if no code was provided,
|
||||
or to override the phone that was previously used with
|
||||
these requests.
|
||||
|
||||
code (`str` | `int`):
|
||||
The code that Telegram sent. Note that if you have sent this
|
||||
code through the application itself it will immediately
|
||||
expire. If you want to send the code, obfuscate it somehow.
|
||||
If you're not doing any of this you can ignore this note.
|
||||
|
||||
password (`str`):
|
||||
2FA password, should be used if a previous call raised
|
||||
``SessionPasswordNeededError``.
|
||||
|
||||
bot_token (`str`):
|
||||
Used to sign in as a bot. Not all requests will be available.
|
||||
This should be the hash the `@BotFather <https://t.me/BotFather>`_
|
||||
gave you.
|
||||
|
||||
phone_code_hash (`str`, optional):
|
||||
The hash returned by `send_code_request`. This can be left as
|
||||
`None` to use the last hash known for the phone to be used.
|
||||
|
||||
Returns
|
||||
The signed in user, or the information about
|
||||
:meth:`send_code_request`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
phone = '+34 123 123 123'
|
||||
await client.sign_in(phone) # send code
|
||||
|
||||
code = input('enter code: ')
|
||||
await client.sign_in(phone, code)
|
||||
"""
|
||||
me = await self.get_me()
|
||||
if me:
|
||||
return me
|
||||
|
||||
if phone and not code and not password:
|
||||
return await self.send_code_request(phone)
|
||||
elif code:
|
||||
phone, phone_code_hash = \
|
||||
self._parse_phone_and_hash(phone, phone_code_hash)
|
||||
|
||||
# May raise PhoneCodeEmptyError, PhoneCodeExpiredError,
|
||||
# PhoneCodeHashEmptyError or PhoneCodeInvalidError.
|
||||
request = functions.auth.SignInRequest(
|
||||
phone, phone_code_hash, str(code)
|
||||
)
|
||||
elif password:
|
||||
pwd = await self(functions.account.GetPasswordRequest())
|
||||
request = functions.auth.CheckPasswordRequest(
|
||||
pwd_mod.compute_check(pwd, password)
|
||||
)
|
||||
elif bot_token:
|
||||
request = functions.auth.ImportBotAuthorizationRequest(
|
||||
flags=0, bot_auth_token=bot_token,
|
||||
api_id=self.api_id, api_hash=self.api_hash
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
'You must provide a phone and a code the first time, '
|
||||
'and a password only if an RPCError was raised before.'
|
||||
)
|
||||
|
||||
try:
|
||||
result = await self(request)
|
||||
except errors.PhoneCodeExpiredError:
|
||||
self._phone_code_hash.pop(phone, None)
|
||||
raise
|
||||
|
||||
if isinstance(result, types.auth.AuthorizationSignUpRequired):
|
||||
# Emulate pre-layer 104 behaviour
|
||||
self._tos = result.terms_of_service
|
||||
raise errors.PhoneNumberUnoccupiedError(request=request)
|
||||
|
||||
return await self._on_login(result.user)
|
||||
|
||||
async def sign_up(
|
||||
self: 'TelegramClient',
|
||||
code: typing.Union[str, int],
|
||||
first_name: str,
|
||||
last_name: str = '',
|
||||
*,
|
||||
phone: str = None,
|
||||
phone_code_hash: str = None) -> 'types.User':
|
||||
"""
|
||||
This method can no longer be used, and will immediately raise a ``ValueError``.
|
||||
See `issue #4050 <https://github.com/LonamiWebs/Telethon/issues/4050>`_ for context.
|
||||
"""
|
||||
raise ValueError('Third-party applications cannot sign up for Telegram. See https://github.com/LonamiWebs/Telethon/issues/4050 for details')
|
||||
|
||||
async def _on_login(self, user):
|
||||
"""
|
||||
Callback called whenever the login or sign up process completes.
|
||||
|
||||
Returns the input user parameter.
|
||||
"""
|
||||
self._mb_entity_cache.set_self_user(user.id, user.bot, user.access_hash)
|
||||
self._authorized = True
|
||||
|
||||
state = await self(functions.updates.GetStateRequest())
|
||||
self._message_box.load(SessionState(0, 0, 0, state.pts, state.qts, int(state.date.timestamp()), state.seq, 0), [])
|
||||
|
||||
return user
|
||||
|
||||
async def send_code_request(
|
||||
self: 'TelegramClient',
|
||||
phone: str,
|
||||
*,
|
||||
force_sms: bool = False,
|
||||
_retry_count: int = 0) -> 'types.auth.SentCode':
|
||||
"""
|
||||
Sends the Telegram code needed to login to the given phone number.
|
||||
|
||||
Arguments
|
||||
phone (`str` | `int`):
|
||||
The phone to which the code will be sent.
|
||||
|
||||
force_sms (`bool`, optional):
|
||||
Whether to force sending as SMS. This has been deprecated.
|
||||
See `issue #4050 <https://github.com/LonamiWebs/Telethon/issues/4050>`_ for context.
|
||||
|
||||
Returns
|
||||
An instance of :tl:`SentCode`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
phone = '+34 123 123 123'
|
||||
sent = await client.send_code_request(phone)
|
||||
print(sent)
|
||||
"""
|
||||
if force_sms:
|
||||
warnings.warn('force_sms has been deprecated and no longer works')
|
||||
force_sms = False
|
||||
|
||||
result = None
|
||||
phone = utils.parse_phone(phone) or self._phone
|
||||
phone_hash = self._phone_code_hash.get(phone)
|
||||
|
||||
if not phone_hash:
|
||||
try:
|
||||
result = await self(functions.auth.SendCodeRequest(
|
||||
phone, self.api_id, self.api_hash, types.CodeSettings()))
|
||||
except errors.AuthRestartError:
|
||||
if _retry_count > 2:
|
||||
raise
|
||||
return await self.send_code_request(
|
||||
phone, force_sms=force_sms, _retry_count=_retry_count+1)
|
||||
|
||||
# TODO figure out when/if/how this can happen
|
||||
if isinstance(result, types.auth.SentCodeSuccess):
|
||||
raise RuntimeError('logged in right after sending the code')
|
||||
|
||||
# If we already sent a SMS, do not resend the code (hash may be empty)
|
||||
if isinstance(result.type, types.auth.SentCodeTypeSms):
|
||||
force_sms = False
|
||||
|
||||
# phone_code_hash may be empty, if it is, do not save it (#1283)
|
||||
if result.phone_code_hash:
|
||||
self._phone_code_hash[phone] = phone_hash = result.phone_code_hash
|
||||
else:
|
||||
force_sms = True
|
||||
|
||||
self._phone = phone
|
||||
|
||||
if force_sms:
|
||||
try:
|
||||
result = await self(
|
||||
functions.auth.ResendCodeRequest(phone, phone_hash))
|
||||
except errors.PhoneCodeExpiredError:
|
||||
if _retry_count > 2:
|
||||
raise
|
||||
self._phone_code_hash.pop(phone, None)
|
||||
self._log[__name__].info(
|
||||
"Phone code expired in ResendCodeRequest, requesting a new code"
|
||||
)
|
||||
return await self.send_code_request(
|
||||
phone, force_sms=False, _retry_count=_retry_count+1)
|
||||
|
||||
if isinstance(result, types.auth.SentCodeSuccess):
|
||||
raise RuntimeError('logged in right after resending the code')
|
||||
|
||||
self._phone_code_hash[phone] = result.phone_code_hash
|
||||
|
||||
return result
|
||||
|
||||
async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin:
|
||||
"""
|
||||
Initiates the QR login procedure.
|
||||
|
||||
Note that you must be connected before invoking this, as with any
|
||||
other request.
|
||||
|
||||
It is up to the caller to decide how to present the code to the user,
|
||||
whether it's the URL, using the token bytes directly, or generating
|
||||
a QR code and displaying it by other means.
|
||||
|
||||
See the documentation for `QRLogin` to see how to proceed after this.
|
||||
|
||||
Arguments
|
||||
ignored_ids (List[`int`]):
|
||||
List of already logged-in user IDs, to prevent logging in
|
||||
twice with the same user.
|
||||
|
||||
Returns
|
||||
An instance of `QRLogin`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
def display_url_as_qr(url):
|
||||
pass # do whatever to show url as a qr to the user
|
||||
|
||||
qr_login = await client.qr_login()
|
||||
display_url_as_qr(qr_login.url)
|
||||
|
||||
# Important! You need to wait for the login to complete!
|
||||
await qr_login.wait()
|
||||
|
||||
# If you have 2FA enabled, `wait` will raise `telethon.errors.SessionPasswordNeededError`.
|
||||
# You should except that error and call `sign_in` with the password if this happens.
|
||||
"""
|
||||
qr_login = custom.QRLogin(self, ignored_ids or [])
|
||||
await qr_login.recreate()
|
||||
return qr_login
|
||||
|
||||
async def log_out(self: 'TelegramClient') -> bool:
|
||||
"""
|
||||
Logs out Telegram and deletes the current ``*.session`` file.
|
||||
|
||||
The client is unusable after logging out and a new instance should be created.
|
||||
|
||||
Returns
|
||||
`True` if the operation was successful.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Note: you will need to login again!
|
||||
await client.log_out()
|
||||
"""
|
||||
try:
|
||||
await self(functions.auth.LogOutRequest())
|
||||
except errors.RPCError:
|
||||
return False
|
||||
|
||||
self._mb_entity_cache.set_self_user(None, None, None)
|
||||
self._authorized = False
|
||||
|
||||
await self.disconnect()
|
||||
self.session.delete()
|
||||
self.session = None
|
||||
return True
|
||||
|
||||
async def edit_2fa(
|
||||
self: 'TelegramClient',
|
||||
current_password: str = None,
|
||||
new_password: str = None,
|
||||
*,
|
||||
hint: str = '',
|
||||
email: str = None,
|
||||
email_code_callback: typing.Callable[[int], str] = None) -> bool:
|
||||
"""
|
||||
Changes the 2FA settings of the logged in user.
|
||||
|
||||
Review carefully the parameter explanations before using this method.
|
||||
|
||||
Note that this method may be *incredibly* slow depending on the
|
||||
prime numbers that must be used during the process to make sure
|
||||
that everything is safe.
|
||||
|
||||
Has no effect if both current and new password are omitted.
|
||||
|
||||
Arguments
|
||||
current_password (`str`, optional):
|
||||
The current password, to authorize changing to ``new_password``.
|
||||
Must be set if changing existing 2FA settings.
|
||||
Must **not** be set if 2FA is currently disabled.
|
||||
Passing this by itself will remove 2FA (if correct).
|
||||
|
||||
new_password (`str`, optional):
|
||||
The password to set as 2FA.
|
||||
If 2FA was already enabled, ``current_password`` **must** be set.
|
||||
Leaving this blank or `None` will remove the password.
|
||||
|
||||
hint (`str`, optional):
|
||||
Hint to be displayed by Telegram when it asks for 2FA.
|
||||
Leaving unspecified is highly discouraged.
|
||||
Has no effect if ``new_password`` is not set.
|
||||
|
||||
email (`str`, optional):
|
||||
Recovery and verification email. If present, you must also
|
||||
set `email_code_callback`, else it raises ``ValueError``.
|
||||
|
||||
email_code_callback (`callable`, optional):
|
||||
If an email is provided, a callback that returns the code sent
|
||||
to it must also be set. This callback may be asynchronous.
|
||||
It should return a string with the code. The length of the
|
||||
code will be passed to the callback as an input parameter.
|
||||
|
||||
If the callback returns an invalid code, it will raise
|
||||
``CodeInvalidError``.
|
||||
|
||||
Returns
|
||||
`True` if successful, `False` otherwise.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Setting a password for your account which didn't have
|
||||
await client.edit_2fa(new_password='I_<3_Telethon')
|
||||
|
||||
# Removing the password
|
||||
await client.edit_2fa(current_password='I_<3_Telethon')
|
||||
"""
|
||||
if new_password is None and current_password is None:
|
||||
return False
|
||||
|
||||
if email and not callable(email_code_callback):
|
||||
raise ValueError('email present without email_code_callback')
|
||||
|
||||
pwd = await self(functions.account.GetPasswordRequest())
|
||||
pwd.new_algo.salt1 += os.urandom(32)
|
||||
assert isinstance(pwd, types.account.Password)
|
||||
if not pwd.has_password and current_password:
|
||||
current_password = None
|
||||
|
||||
if current_password:
|
||||
password = pwd_mod.compute_check(pwd, current_password)
|
||||
else:
|
||||
password = types.InputCheckPasswordEmpty()
|
||||
|
||||
if new_password:
|
||||
new_password_hash = pwd_mod.compute_digest(
|
||||
pwd.new_algo, new_password)
|
||||
else:
|
||||
new_password_hash = b''
|
||||
|
||||
try:
|
||||
await self(functions.account.UpdatePasswordSettingsRequest(
|
||||
password=password,
|
||||
new_settings=types.account.PasswordInputSettings(
|
||||
new_algo=pwd.new_algo,
|
||||
new_password_hash=new_password_hash,
|
||||
hint=hint,
|
||||
email=email,
|
||||
new_secure_settings=None
|
||||
)
|
||||
))
|
||||
except errors.EmailUnconfirmedError as e:
|
||||
code = email_code_callback(e.code_length)
|
||||
if inspect.isawaitable(code):
|
||||
code = await code
|
||||
|
||||
code = str(code)
|
||||
await self(functions.account.ConfirmPasswordEmailRequest(code))
|
||||
|
||||
return True
|
||||
|
||||
# endregion
|
||||
|
||||
# region with blocks
|
||||
|
||||
async def __aenter__(self):
|
||||
return await self.start()
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
await self.disconnect()
|
||||
|
||||
__enter__ = helpers._sync_enter
|
||||
__exit__ = helpers._sync_exit
|
||||
|
||||
# endregion
|
|
@ -1,72 +0,0 @@
|
|||
import typing
|
||||
|
||||
from .. import hints
|
||||
from ..tl import types, functions, custom
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
||||
|
||||
class BotMethods:
|
||||
async def inline_query(
|
||||
self: 'TelegramClient',
|
||||
bot: 'hints.EntityLike',
|
||||
query: str,
|
||||
*,
|
||||
entity: 'hints.EntityLike' = None,
|
||||
offset: str = None,
|
||||
geo_point: 'types.GeoPoint' = None) -> custom.InlineResults:
|
||||
"""
|
||||
Makes an inline query to the specified bot (``@vote New Poll``).
|
||||
|
||||
Arguments
|
||||
bot (`entity`):
|
||||
The bot entity to which the inline query should be made.
|
||||
|
||||
query (`str`):
|
||||
The query that should be made to the bot.
|
||||
|
||||
entity (`entity`, optional):
|
||||
The entity where the inline query is being made from. Certain
|
||||
bots use this to display different results depending on where
|
||||
it's used, such as private chats, groups or channels.
|
||||
|
||||
If specified, it will also be the default entity where the
|
||||
message will be sent after clicked. Otherwise, the "empty
|
||||
peer" will be used, which some bots may not handle correctly.
|
||||
|
||||
offset (`str`, optional):
|
||||
The string offset to use for the bot.
|
||||
|
||||
geo_point (:tl:`GeoPoint`, optional)
|
||||
The geo point location information to send to the bot
|
||||
for localised results. Available under some bots.
|
||||
|
||||
Returns
|
||||
A list of `custom.InlineResult
|
||||
<telethon.tl.custom.inlineresult.InlineResult>`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Make an inline query to @like
|
||||
results = await client.inline_query('like', 'Do you like Telethon?')
|
||||
|
||||
# Send the first result to some chat
|
||||
message = await results[0].click('TelethonOffTopic')
|
||||
"""
|
||||
bot = await self.get_input_entity(bot)
|
||||
if entity:
|
||||
peer = await self.get_input_entity(entity)
|
||||
else:
|
||||
peer = types.InputPeerEmpty()
|
||||
|
||||
result = await self(functions.messages.GetInlineBotResultsRequest(
|
||||
bot=bot,
|
||||
peer=peer,
|
||||
query=query,
|
||||
offset=offset or '',
|
||||
geo_point=geo_point
|
||||
))
|
||||
|
||||
return custom.InlineResults(self, result, entity=peer if entity else None)
|
|
@ -1,96 +0,0 @@
|
|||
import typing
|
||||
|
||||
from .. import utils, hints
|
||||
from ..tl import types, custom
|
||||
|
||||
|
||||
class ButtonMethods:
|
||||
@staticmethod
|
||||
def build_reply_markup(
|
||||
buttons: 'typing.Optional[hints.MarkupLike]',
|
||||
inline_only: bool = False) -> 'typing.Optional[types.TypeReplyMarkup]':
|
||||
"""
|
||||
Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for
|
||||
the given buttons.
|
||||
|
||||
Does nothing if either no buttons are provided or the provided
|
||||
argument is already a reply markup.
|
||||
|
||||
You should consider using this method if you are going to reuse
|
||||
the markup very often. Otherwise, it is not necessary.
|
||||
|
||||
This method is **not** asynchronous (don't use ``await`` on it).
|
||||
|
||||
Arguments
|
||||
buttons (`hints.MarkupLike`):
|
||||
The button, list of buttons, array of buttons or markup
|
||||
to convert into a markup.
|
||||
|
||||
inline_only (`bool`, optional):
|
||||
Whether the buttons **must** be inline buttons only or not.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import Button
|
||||
|
||||
markup = client.build_reply_markup(Button.inline('hi'))
|
||||
# later
|
||||
await client.send_message(chat, 'click me', buttons=markup)
|
||||
"""
|
||||
if buttons is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
if buttons.SUBCLASS_OF_ID == 0xe2e10ef2:
|
||||
return buttons # crc32(b'ReplyMarkup'):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if not utils.is_list_like(buttons):
|
||||
buttons = [[buttons]]
|
||||
elif not buttons or not utils.is_list_like(buttons[0]):
|
||||
buttons = [buttons]
|
||||
|
||||
is_inline = False
|
||||
is_normal = False
|
||||
resize = None
|
||||
single_use = None
|
||||
selective = None
|
||||
|
||||
rows = []
|
||||
for row in buttons:
|
||||
current = []
|
||||
for button in row:
|
||||
if isinstance(button, custom.Button):
|
||||
if button.resize is not None:
|
||||
resize = button.resize
|
||||
if button.single_use is not None:
|
||||
single_use = button.single_use
|
||||
if button.selective is not None:
|
||||
selective = button.selective
|
||||
|
||||
button = button.button
|
||||
elif isinstance(button, custom.MessageButton):
|
||||
button = button.button
|
||||
|
||||
inline = custom.Button._is_inline(button)
|
||||
is_inline |= inline
|
||||
is_normal |= not inline
|
||||
|
||||
if button.SUBCLASS_OF_ID == 0xbad74a3:
|
||||
# 0xbad74a3 == crc32(b'KeyboardButton')
|
||||
current.append(button)
|
||||
|
||||
if current:
|
||||
rows.append(types.KeyboardButtonRow(current))
|
||||
|
||||
if inline_only and is_normal:
|
||||
raise ValueError('You cannot use non-inline buttons here')
|
||||
elif is_inline == is_normal and is_normal:
|
||||
raise ValueError('You cannot mix inline with normal buttons')
|
||||
elif is_inline:
|
||||
return types.ReplyInlineMarkup(rows)
|
||||
# elif is_normal:
|
||||
return types.ReplyKeyboardMarkup(
|
||||
rows, resize=resize, single_use=single_use, selective=selective)
|
File diff suppressed because it is too large
Load Diff
|
@ -1,610 +0,0 @@
|
|||
import asyncio
|
||||
import inspect
|
||||
import itertools
|
||||
import typing
|
||||
|
||||
from .. import helpers, utils, hints, errors
|
||||
from ..requestiter import RequestIter
|
||||
from ..tl import types, functions, custom
|
||||
|
||||
_MAX_CHUNK_SIZE = 100
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
||||
|
||||
def _dialog_message_key(peer, message_id):
|
||||
"""
|
||||
Get the key to get messages from a dialog.
|
||||
|
||||
We cannot just use the message ID because channels share message IDs,
|
||||
and the peer ID is required to distinguish between them. But it is not
|
||||
necessary in small group chats and private chats.
|
||||
"""
|
||||
return (peer.channel_id if isinstance(peer, types.PeerChannel) else None), message_id
|
||||
|
||||
|
||||
class _DialogsIter(RequestIter):
|
||||
async def _init(
|
||||
self, offset_date, offset_id, offset_peer, ignore_pinned, ignore_migrated, folder
|
||||
):
|
||||
self.request = functions.messages.GetDialogsRequest(
|
||||
offset_date=offset_date,
|
||||
offset_id=offset_id,
|
||||
offset_peer=offset_peer,
|
||||
limit=1,
|
||||
hash=0,
|
||||
exclude_pinned=ignore_pinned,
|
||||
folder_id=folder
|
||||
)
|
||||
|
||||
if self.limit <= 0:
|
||||
# Special case, get a single dialog and determine count
|
||||
dialogs = await self.client(self.request)
|
||||
self.total = getattr(dialogs, 'count', len(dialogs.dialogs))
|
||||
raise StopAsyncIteration
|
||||
|
||||
self.seen = set()
|
||||
self.offset_date = offset_date
|
||||
self.ignore_migrated = ignore_migrated
|
||||
|
||||
async def _load_next_chunk(self):
|
||||
self.request.limit = min(self.left, _MAX_CHUNK_SIZE)
|
||||
r = await self.client(self.request)
|
||||
|
||||
self.total = getattr(r, 'count', len(r.dialogs))
|
||||
|
||||
entities = {utils.get_peer_id(x): x
|
||||
for x in itertools.chain(r.users, r.chats)
|
||||
if not isinstance(x, (types.UserEmpty, types.ChatEmpty))}
|
||||
|
||||
self.client._mb_entity_cache.extend(r.users, r.chats)
|
||||
|
||||
messages = {}
|
||||
for m in r.messages:
|
||||
m._finish_init(self.client, entities, None)
|
||||
messages[_dialog_message_key(m.peer_id, m.id)] = m
|
||||
|
||||
for d in r.dialogs:
|
||||
# We check the offset date here because Telegram may ignore it
|
||||
message = messages.get(_dialog_message_key(d.peer, d.top_message))
|
||||
if self.offset_date:
|
||||
date = getattr(message, 'date', None)
|
||||
if not date or date.timestamp() > self.offset_date.timestamp():
|
||||
continue
|
||||
|
||||
peer_id = utils.get_peer_id(d.peer)
|
||||
if peer_id not in self.seen:
|
||||
self.seen.add(peer_id)
|
||||
if peer_id not in entities:
|
||||
# > In which case can a UserEmpty appear in the list of banned members?
|
||||
# > In a very rare cases. This is possible but isn't an expected behavior.
|
||||
# Real world example: https://t.me/TelethonChat/271471
|
||||
continue
|
||||
|
||||
cd = custom.Dialog(self.client, d, entities, message)
|
||||
if cd.dialog.pts:
|
||||
self.client._message_box.try_set_channel_state(
|
||||
utils.get_peer_id(d.peer, add_mark=False), cd.dialog.pts)
|
||||
|
||||
if not self.ignore_migrated or getattr(
|
||||
cd.entity, 'migrated_to', None) is None:
|
||||
self.buffer.append(cd)
|
||||
|
||||
if not self.buffer or len(r.dialogs) < self.request.limit\
|
||||
or not isinstance(r, types.messages.DialogsSlice):
|
||||
# Buffer being empty means all returned dialogs were skipped (due to offsets).
|
||||
# Less than we requested means we reached the end, or
|
||||
# we didn't get a DialogsSlice which means we got all.
|
||||
return True
|
||||
|
||||
# We can't use `messages[-1]` as the offset ID / date.
|
||||
# Why? Because pinned dialogs will mess with the order
|
||||
# in this list. Instead, we find the last dialog which
|
||||
# has a message, and use it as an offset.
|
||||
last_message = next(filter(None, (
|
||||
messages.get(_dialog_message_key(d.peer, d.top_message))
|
||||
for d in reversed(r.dialogs)
|
||||
)), None)
|
||||
|
||||
self.request.exclude_pinned = True
|
||||
self.request.offset_id = last_message.id if last_message else 0
|
||||
self.request.offset_date = last_message.date if last_message else None
|
||||
self.request.offset_peer = self.buffer[-1].input_entity
|
||||
|
||||
|
||||
class _DraftsIter(RequestIter):
|
||||
async def _init(self, entities, **kwargs):
|
||||
if not entities:
|
||||
r = await self.client(functions.messages.GetAllDraftsRequest())
|
||||
items = r.updates
|
||||
else:
|
||||
peers = []
|
||||
for entity in entities:
|
||||
peers.append(types.InputDialogPeer(
|
||||
await self.client.get_input_entity(entity)))
|
||||
|
||||
r = await self.client(functions.messages.GetPeerDialogsRequest(peers))
|
||||
items = r.dialogs
|
||||
|
||||
# TODO Maybe there should be a helper method for this?
|
||||
entities = {utils.get_peer_id(x): x
|
||||
for x in itertools.chain(r.users, r.chats)}
|
||||
|
||||
self.buffer.extend(
|
||||
custom.Draft(self.client, entities[utils.get_peer_id(d.peer)], d.draft)
|
||||
for d in items
|
||||
)
|
||||
|
||||
async def _load_next_chunk(self):
|
||||
return []
|
||||
|
||||
|
||||
class DialogMethods:
|
||||
|
||||
# region Public methods
|
||||
|
||||
def iter_dialogs(
|
||||
self: 'TelegramClient',
|
||||
limit: float = None,
|
||||
*,
|
||||
offset_date: 'hints.DateLike' = None,
|
||||
offset_id: int = 0,
|
||||
offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(),
|
||||
ignore_pinned: bool = False,
|
||||
ignore_migrated: bool = False,
|
||||
folder: int = None,
|
||||
archived: bool = None
|
||||
) -> _DialogsIter:
|
||||
"""
|
||||
Iterator over the dialogs (open conversations/subscribed channels).
|
||||
|
||||
The order is the same as the one seen in official applications
|
||||
(first pinned, them from those with the most recent message to
|
||||
those with the oldest message).
|
||||
|
||||
Arguments
|
||||
limit (`int` | `None`):
|
||||
How many dialogs to be retrieved as maximum. Can be set to
|
||||
`None` to retrieve all dialogs. Note that this may take
|
||||
whole minutes if you have hundreds of dialogs, as Telegram
|
||||
will tell the library to slow down through a
|
||||
``FloodWaitError``.
|
||||
|
||||
offset_date (`datetime`, optional):
|
||||
The offset date to be used.
|
||||
|
||||
offset_id (`int`, optional):
|
||||
The message ID to be used as an offset.
|
||||
|
||||
offset_peer (:tl:`InputPeer`, optional):
|
||||
The peer to be used as an offset.
|
||||
|
||||
ignore_pinned (`bool`, optional):
|
||||
Whether pinned dialogs should be ignored or not.
|
||||
When set to `True`, these won't be yielded at all.
|
||||
|
||||
ignore_migrated (`bool`, optional):
|
||||
Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel`
|
||||
should be included or not. By default all the chats in your
|
||||
dialogs are returned, but setting this to `True` will ignore
|
||||
(i.e. skip) them in the same way official applications do.
|
||||
|
||||
folder (`int`, optional):
|
||||
The folder from which the dialogs should be retrieved.
|
||||
|
||||
If left unspecified, all dialogs (including those from
|
||||
folders) will be returned.
|
||||
|
||||
If set to ``0``, all dialogs that don't belong to any
|
||||
folder will be returned.
|
||||
|
||||
If set to a folder number like ``1``, only those from
|
||||
said folder will be returned.
|
||||
|
||||
By default Telegram assigns the folder ID ``1`` to
|
||||
archived chats, so you should use that if you need
|
||||
to fetch the archived dialogs.
|
||||
|
||||
archived (`bool`, optional):
|
||||
Alias for `folder`. If unspecified, all will be returned,
|
||||
`False` implies ``folder=0`` and `True` implies ``folder=1``.
|
||||
Yields
|
||||
Instances of `Dialog <telethon.tl.custom.dialog.Dialog>`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Print all dialog IDs and the title, nicely formatted
|
||||
async for dialog in client.iter_dialogs():
|
||||
print('{:>14}: {}'.format(dialog.id, dialog.title))
|
||||
"""
|
||||
if archived is not None:
|
||||
folder = 1 if archived else 0
|
||||
|
||||
return _DialogsIter(
|
||||
self,
|
||||
limit,
|
||||
offset_date=offset_date,
|
||||
offset_id=offset_id,
|
||||
offset_peer=offset_peer,
|
||||
ignore_pinned=ignore_pinned,
|
||||
ignore_migrated=ignore_migrated,
|
||||
folder=folder
|
||||
)
|
||||
|
||||
async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList':
|
||||
"""
|
||||
Same as `iter_dialogs()`, but returns a
|
||||
`TotalList <telethon.helpers.TotalList>` instead.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Get all open conversation, print the title of the first
|
||||
dialogs = await client.get_dialogs()
|
||||
first = dialogs[0]
|
||||
print(first.title)
|
||||
|
||||
# Use the dialog somewhere else
|
||||
await client.send_message(first, 'hi')
|
||||
|
||||
# Getting only non-archived dialogs (both equivalent)
|
||||
non_archived = await client.get_dialogs(folder=0)
|
||||
non_archived = await client.get_dialogs(archived=False)
|
||||
|
||||
# Getting only archived dialogs (both equivalent)
|
||||
archived = await client.get_dialogs(folder=1)
|
||||
archived = await client.get_dialogs(archived=True)
|
||||
"""
|
||||
return await self.iter_dialogs(*args, **kwargs).collect()
|
||||
|
||||
get_dialogs.__signature__ = inspect.signature(iter_dialogs)
|
||||
|
||||
def iter_drafts(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntitiesLike' = None
|
||||
) -> _DraftsIter:
|
||||
"""
|
||||
Iterator over draft messages.
|
||||
|
||||
The order is unspecified.
|
||||
|
||||
Arguments
|
||||
entity (`hints.EntitiesLike`, optional):
|
||||
The entity or entities for which to fetch the draft messages.
|
||||
If left unspecified, all draft messages will be returned.
|
||||
|
||||
Yields
|
||||
Instances of `Draft <telethon.tl.custom.draft.Draft>`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Clear all drafts
|
||||
async for draft in client.get_drafts():
|
||||
await draft.delete()
|
||||
|
||||
# Getting the drafts with 'bot1' and 'bot2'
|
||||
async for draft in client.iter_drafts(['bot1', 'bot2']):
|
||||
print(draft.text)
|
||||
"""
|
||||
if entity and not utils.is_list_like(entity):
|
||||
entity = (entity,)
|
||||
|
||||
# TODO Passing a limit here makes no sense
|
||||
return _DraftsIter(self, None, entities=entity)
|
||||
|
||||
async def get_drafts(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntitiesLike' = None
|
||||
) -> 'hints.TotalList':
|
||||
"""
|
||||
Same as `iter_drafts()`, but returns a list instead.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Get drafts, print the text of the first
|
||||
drafts = await client.get_drafts()
|
||||
print(drafts[0].text)
|
||||
|
||||
# Get the draft in your chat
|
||||
draft = await client.get_drafts('me')
|
||||
print(drafts.text)
|
||||
"""
|
||||
items = await self.iter_drafts(entity).collect()
|
||||
if not entity or utils.is_list_like(entity):
|
||||
return items
|
||||
else:
|
||||
return items[0]
|
||||
|
||||
async def edit_folder(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntitiesLike' = None,
|
||||
folder: typing.Union[int, typing.Sequence[int]] = None,
|
||||
*,
|
||||
unpack=None
|
||||
) -> types.Updates:
|
||||
"""
|
||||
Edits the folder used by one or more dialogs to archive them.
|
||||
|
||||
Arguments
|
||||
entity (entities):
|
||||
The entity or list of entities to move to the desired
|
||||
archive folder.
|
||||
|
||||
folder (`int`):
|
||||
The folder to which the dialog should be archived to.
|
||||
|
||||
If you want to "archive" a dialog, use ``folder=1``.
|
||||
|
||||
If you want to "un-archive" it, use ``folder=0``.
|
||||
|
||||
You may also pass a list with the same length as
|
||||
`entities` if you want to control where each entity
|
||||
will go.
|
||||
|
||||
unpack (`int`, optional):
|
||||
If you want to unpack an archived folder, set this
|
||||
parameter to the folder number that you want to
|
||||
delete.
|
||||
|
||||
When you unpack a folder, all the dialogs inside are
|
||||
moved to the folder number 0.
|
||||
|
||||
You can only use this parameter if the other two
|
||||
are not set.
|
||||
|
||||
Returns
|
||||
The :tl:`Updates` object that the request produces.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Archiving the first 5 dialogs
|
||||
dialogs = await client.get_dialogs(5)
|
||||
await client.edit_folder(dialogs, 1)
|
||||
|
||||
# Un-archiving the third dialog (archiving to folder 0)
|
||||
await client.edit_folder(dialog[2], 0)
|
||||
|
||||
# Moving the first dialog to folder 0 and the second to 1
|
||||
dialogs = await client.get_dialogs(2)
|
||||
await client.edit_folder(dialogs, [0, 1])
|
||||
|
||||
# Un-archiving all dialogs
|
||||
await client.edit_folder(unpack=1)
|
||||
"""
|
||||
if (entity is None) == (unpack is None):
|
||||
raise ValueError('You can only set either entities or unpack, not both')
|
||||
|
||||
if unpack is not None:
|
||||
return await self(functions.folders.DeleteFolderRequest(
|
||||
folder_id=unpack
|
||||
))
|
||||
|
||||
if not utils.is_list_like(entity):
|
||||
entities = [await self.get_input_entity(entity)]
|
||||
else:
|
||||
entities = await asyncio.gather(
|
||||
*(self.get_input_entity(x) for x in entity))
|
||||
|
||||
if folder is None:
|
||||
raise ValueError('You must specify a folder')
|
||||
elif not utils.is_list_like(folder):
|
||||
folder = [folder] * len(entities)
|
||||
elif len(entities) != len(folder):
|
||||
raise ValueError('Number of folders does not match number of entities')
|
||||
|
||||
return await self(functions.folders.EditPeerFoldersRequest([
|
||||
types.InputFolderPeer(x, folder_id=y)
|
||||
for x, y in zip(entities, folder)
|
||||
]))
|
||||
|
||||
async def delete_dialog(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntityLike',
|
||||
*,
|
||||
revoke: bool = False
|
||||
):
|
||||
"""
|
||||
Deletes a dialog (leaves a chat or channel).
|
||||
|
||||
This method can be used as a user and as a bot. However,
|
||||
bots will only be able to use it to leave groups and channels
|
||||
(trying to delete a private conversation will do nothing).
|
||||
|
||||
See also `Dialog.delete() <telethon.tl.custom.dialog.Dialog.delete>`.
|
||||
|
||||
Arguments
|
||||
entity (entities):
|
||||
The entity of the dialog to delete. If it's a chat or
|
||||
channel, you will leave it. Note that the chat itself
|
||||
is not deleted, only the dialog, because you left it.
|
||||
|
||||
revoke (`bool`, optional):
|
||||
On private chats, you may revoke the messages from
|
||||
the other peer too. By default, it's `False`. Set
|
||||
it to `True` to delete the history for both.
|
||||
|
||||
This makes no difference for bot accounts, who can
|
||||
only leave groups and channels.
|
||||
|
||||
Returns
|
||||
The :tl:`Updates` object that the request produces,
|
||||
or nothing for private conversations.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Deleting the first dialog
|
||||
dialogs = await client.get_dialogs(5)
|
||||
await client.delete_dialog(dialogs[0])
|
||||
|
||||
# Leaving a channel by username
|
||||
await client.delete_dialog('username')
|
||||
"""
|
||||
# If we have enough information (`Dialog.delete` gives it to us),
|
||||
# then we know we don't have to kick ourselves in deactivated chats.
|
||||
if isinstance(entity, types.Chat):
|
||||
deactivated = entity.deactivated
|
||||
else:
|
||||
deactivated = False
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
ty = helpers._entity_type(entity)
|
||||
if ty == helpers._EntityType.CHANNEL:
|
||||
return await self(functions.channels.LeaveChannelRequest(entity))
|
||||
|
||||
if ty == helpers._EntityType.CHAT and not deactivated:
|
||||
try:
|
||||
result = await self(functions.messages.DeleteChatUserRequest(
|
||||
entity.chat_id, types.InputUserSelf(), revoke_history=revoke
|
||||
))
|
||||
except errors.PeerIdInvalidError:
|
||||
# Happens if we didn't have the deactivated information
|
||||
result = None
|
||||
else:
|
||||
result = None
|
||||
|
||||
if not await self.is_bot():
|
||||
await self(functions.messages.DeleteHistoryRequest(entity, 0, revoke=revoke))
|
||||
|
||||
return result
|
||||
|
||||
def conversation(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntityLike',
|
||||
*,
|
||||
timeout: float = 60,
|
||||
total_timeout: float = None,
|
||||
max_messages: int = 100,
|
||||
exclusive: bool = True,
|
||||
replies_are_responses: bool = True) -> custom.Conversation:
|
||||
"""
|
||||
Creates a `Conversation <telethon.tl.custom.conversation.Conversation>`
|
||||
with the given entity.
|
||||
|
||||
.. note::
|
||||
|
||||
This Conversation API has certain shortcomings, such as lacking
|
||||
persistence, poor interaction with other event handlers, and
|
||||
overcomplicated usage for anything beyond the simplest case.
|
||||
|
||||
If you plan to interact with a bot without handlers, this works
|
||||
fine, but when running a bot yourself, you may instead prefer
|
||||
to follow the advice from https://stackoverflow.com/a/62246569/.
|
||||
|
||||
This is not the same as just sending a message to create a "dialog"
|
||||
with them, but rather a way to easily send messages and await for
|
||||
responses or other reactions. Refer to its documentation for more.
|
||||
|
||||
Arguments
|
||||
entity (`entity`):
|
||||
The entity with which a new conversation should be opened.
|
||||
|
||||
timeout (`int` | `float`, optional):
|
||||
The default timeout (in seconds) *per action* to be used. You
|
||||
may also override this timeout on a per-method basis. By
|
||||
default each action can take up to 60 seconds (the value of
|
||||
this timeout).
|
||||
|
||||
total_timeout (`int` | `float`, optional):
|
||||
The total timeout (in seconds) to use for the whole
|
||||
conversation. This takes priority over per-action
|
||||
timeouts. After these many seconds pass, subsequent
|
||||
actions will result in ``asyncio.TimeoutError``.
|
||||
|
||||
max_messages (`int`, optional):
|
||||
The maximum amount of messages this conversation will
|
||||
remember. After these many messages arrive in the
|
||||
specified chat, subsequent actions will result in
|
||||
``ValueError``.
|
||||
|
||||
exclusive (`bool`, optional):
|
||||
By default, conversations are exclusive within a single
|
||||
chat. That means that while a conversation is open in a
|
||||
chat, you can't open another one in the same chat, unless
|
||||
you disable this flag.
|
||||
|
||||
If you try opening an exclusive conversation for
|
||||
a chat where it's already open, it will raise
|
||||
``AlreadyInConversationError``.
|
||||
|
||||
replies_are_responses (`bool`, optional):
|
||||
Whether replies should be treated as responses or not.
|
||||
|
||||
If the setting is enabled, calls to `conv.get_response
|
||||
<telethon.tl.custom.conversation.Conversation.get_response>`
|
||||
and a subsequent call to `conv.get_reply
|
||||
<telethon.tl.custom.conversation.Conversation.get_reply>`
|
||||
will return different messages, otherwise they may return
|
||||
the same message.
|
||||
|
||||
Consider the following scenario with one outgoing message,
|
||||
1, and two incoming messages, the second one replying::
|
||||
|
||||
Hello! <1
|
||||
2> (reply to 1) Hi!
|
||||
3> (reply to 1) How are you?
|
||||
|
||||
And the following code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with client.conversation(chat) as conv:
|
||||
msg1 = await conv.send_message('Hello!')
|
||||
msg2 = await conv.get_response()
|
||||
msg3 = await conv.get_reply()
|
||||
|
||||
With the setting enabled, ``msg2`` will be ``'Hi!'`` and
|
||||
``msg3`` be ``'How are you?'`` since replies are also
|
||||
responses, and a response was already returned.
|
||||
|
||||
With the setting disabled, both ``msg2`` and ``msg3`` will
|
||||
be ``'Hi!'`` since one is a response and also a reply.
|
||||
|
||||
Returns
|
||||
A `Conversation <telethon.tl.custom.conversation.Conversation>`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# <you> denotes outgoing messages you sent
|
||||
# <usr> denotes incoming response messages
|
||||
with bot.conversation(chat) as conv:
|
||||
# <you> Hi!
|
||||
conv.send_message('Hi!')
|
||||
|
||||
# <usr> Hello!
|
||||
hello = conv.get_response()
|
||||
|
||||
# <you> Please tell me your name
|
||||
conv.send_message('Please tell me your name')
|
||||
|
||||
# <usr> ?
|
||||
name = conv.get_response().raw_text
|
||||
|
||||
while not any(x.isalpha() for x in name):
|
||||
# <you> Your name didn't have any letters! Try again
|
||||
conv.send_message("Your name didn't have any letters! Try again")
|
||||
|
||||
# <usr> Human
|
||||
name = conv.get_response().raw_text
|
||||
|
||||
# <you> Thanks Human!
|
||||
conv.send_message('Thanks {}!'.format(name))
|
||||
"""
|
||||
return custom.Conversation(
|
||||
self,
|
||||
entity,
|
||||
timeout=timeout,
|
||||
total_timeout=total_timeout,
|
||||
max_messages=max_messages,
|
||||
exclusive=exclusive,
|
||||
replies_are_responses=replies_are_responses
|
||||
|
||||
)
|
||||
|
||||
# endregion
|
File diff suppressed because it is too large
Load Diff
|
@ -1,233 +0,0 @@
|
|||
import itertools
|
||||
import re
|
||||
import typing
|
||||
|
||||
from .. import helpers, utils
|
||||
from ..tl import types
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
||||
|
||||
class MessageParseMethods:
|
||||
|
||||
# region Public properties
|
||||
|
||||
@property
|
||||
def parse_mode(self: 'TelegramClient'):
|
||||
"""
|
||||
This property is the default parse mode used when sending messages.
|
||||
Defaults to `telethon.extensions.markdown`. It will always
|
||||
be either `None` or an object with ``parse`` and ``unparse``
|
||||
methods.
|
||||
|
||||
When setting a different value it should be one of:
|
||||
|
||||
* Object with ``parse`` and ``unparse`` methods.
|
||||
* A ``callable`` to act as the parse method.
|
||||
* A `str` indicating the ``parse_mode``. For Markdown ``'md'``
|
||||
or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'``
|
||||
may be used.
|
||||
|
||||
The ``parse`` method should be a function accepting a single
|
||||
parameter, the text to parse, and returning a tuple consisting
|
||||
of ``(parsed message str, [MessageEntity instances])``.
|
||||
|
||||
The ``unparse`` method should be the inverse of ``parse`` such
|
||||
that ``assert text == unparse(*parse(text))``.
|
||||
|
||||
See :tl:`MessageEntity` for allowed message entities.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Disabling default formatting
|
||||
client.parse_mode = None
|
||||
|
||||
# Enabling HTML as the default format
|
||||
client.parse_mode = 'html'
|
||||
"""
|
||||
return self._parse_mode
|
||||
|
||||
@parse_mode.setter
|
||||
def parse_mode(self: 'TelegramClient', mode: str):
|
||||
self._parse_mode = utils.sanitize_parse_mode(mode)
|
||||
|
||||
# endregion
|
||||
|
||||
# region Private methods
|
||||
|
||||
async def _replace_with_mention(self: 'TelegramClient', entities, i, user):
|
||||
"""
|
||||
Helper method to replace ``entities[i]`` to mention ``user``,
|
||||
or do nothing if it can't be found.
|
||||
"""
|
||||
try:
|
||||
entities[i] = types.InputMessageEntityMentionName(
|
||||
entities[i].offset, entities[i].length,
|
||||
await self.get_input_entity(user)
|
||||
)
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
async def _parse_message_text(self: 'TelegramClient', message, parse_mode):
|
||||
"""
|
||||
Returns a (parsed message, entities) tuple depending on ``parse_mode``.
|
||||
"""
|
||||
if parse_mode == ():
|
||||
parse_mode = self._parse_mode
|
||||
else:
|
||||
parse_mode = utils.sanitize_parse_mode(parse_mode)
|
||||
|
||||
if not parse_mode:
|
||||
return message, []
|
||||
|
||||
original_message = message
|
||||
message, msg_entities = parse_mode.parse(message)
|
||||
if original_message and not message and not msg_entities:
|
||||
raise ValueError("Failed to parse message")
|
||||
|
||||
for i in reversed(range(len(msg_entities))):
|
||||
e = msg_entities[i]
|
||||
if not e.length:
|
||||
# 0-length MessageEntity is no longer valid #3884.
|
||||
# Because the user can provide their own parser (with reasonable 0-length
|
||||
# entities), strip them here rather than fixing the built-in parsers.
|
||||
del msg_entities[i]
|
||||
elif isinstance(e, types.MessageEntityTextUrl):
|
||||
m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url)
|
||||
if m:
|
||||
user = int(m.group(1)) if m.group(1) else e.url
|
||||
is_mention = await self._replace_with_mention(msg_entities, i, user)
|
||||
if not is_mention:
|
||||
del msg_entities[i]
|
||||
elif isinstance(e, (types.MessageEntityMentionName,
|
||||
types.InputMessageEntityMentionName)):
|
||||
is_mention = await self._replace_with_mention(msg_entities, i, e.user_id)
|
||||
if not is_mention:
|
||||
del msg_entities[i]
|
||||
|
||||
return message, msg_entities
|
||||
|
||||
def _get_response_message(self: 'TelegramClient', request, result, input_chat):
|
||||
"""
|
||||
Extracts the response message known a request and Update result.
|
||||
The request may also be the ID of the message to match.
|
||||
|
||||
If ``request is None`` this method returns ``{id: message}``.
|
||||
|
||||
If ``request.random_id`` is a list, this method returns a list too.
|
||||
"""
|
||||
if isinstance(result, types.UpdateShort):
|
||||
updates = [result.update]
|
||||
entities = {}
|
||||
elif isinstance(result, (types.Updates, types.UpdatesCombined)):
|
||||
updates = result.updates
|
||||
entities = {utils.get_peer_id(x): x
|
||||
for x in
|
||||
itertools.chain(result.users, result.chats)}
|
||||
else:
|
||||
return None
|
||||
|
||||
random_to_id = {}
|
||||
id_to_message = {}
|
||||
for update in updates:
|
||||
if isinstance(update, types.UpdateMessageID):
|
||||
random_to_id[update.random_id] = update.id
|
||||
|
||||
elif isinstance(update, (
|
||||
types.UpdateNewChannelMessage, types.UpdateNewMessage)):
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
|
||||
# Pinning a message with `updatePinnedMessage` seems to
|
||||
# always produce a service message we can't map so return
|
||||
# it directly. The same happens for kicking users.
|
||||
#
|
||||
# It could also be a list (e.g. when sending albums).
|
||||
#
|
||||
# TODO this method is getting messier and messier as time goes on
|
||||
if hasattr(request, 'random_id') or utils.is_list_like(request):
|
||||
id_to_message[update.message.id] = update.message
|
||||
else:
|
||||
return update.message
|
||||
|
||||
elif (isinstance(update, types.UpdateEditMessage)
|
||||
and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL):
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
|
||||
# Live locations use `sendMedia` but Telegram responds with
|
||||
# `updateEditMessage`, which means we won't have `id` field.
|
||||
if hasattr(request, 'random_id'):
|
||||
id_to_message[update.message.id] = update.message
|
||||
elif request.id == update.message.id:
|
||||
return update.message
|
||||
|
||||
elif (isinstance(update, types.UpdateEditChannelMessage)
|
||||
and utils.get_peer_id(request.peer) ==
|
||||
utils.get_peer_id(update.message.peer_id)):
|
||||
if request.id == update.message.id:
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
return update.message
|
||||
|
||||
elif isinstance(update, types.UpdateNewScheduledMessage):
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
# Scheduled IDs may collide with normal IDs. However, for a
|
||||
# single request there *shouldn't* be a mix between "some
|
||||
# scheduled and some not".
|
||||
id_to_message[update.message.id] = update.message
|
||||
|
||||
elif isinstance(update, types.UpdateMessagePoll):
|
||||
if request.media.poll.id == update.poll_id:
|
||||
m = types.Message(
|
||||
id=request.id,
|
||||
peer_id=utils.get_peer(request.peer),
|
||||
media=types.MessageMediaPoll(
|
||||
poll=update.poll,
|
||||
results=update.results
|
||||
)
|
||||
)
|
||||
m._finish_init(self, entities, input_chat)
|
||||
return m
|
||||
|
||||
if request is None:
|
||||
return id_to_message
|
||||
|
||||
random_id = request if isinstance(request, (int, list)) else getattr(request, 'random_id', None)
|
||||
if random_id is None:
|
||||
# Can happen when pinning a message does not actually produce a service message.
|
||||
self._log[__name__].warning(
|
||||
'No random_id in %s to map to, returning None message for %s', request, result)
|
||||
return None
|
||||
|
||||
if not utils.is_list_like(random_id):
|
||||
msg = id_to_message.get(random_to_id.get(random_id))
|
||||
|
||||
if not msg:
|
||||
self._log[__name__].warning(
|
||||
'Request %s had missing message mapping %s', request, result)
|
||||
|
||||
return msg
|
||||
|
||||
try:
|
||||
return [id_to_message[random_to_id[rnd]] for rnd in random_id]
|
||||
except KeyError:
|
||||
# Sometimes forwards fail (`MESSAGE_ID_INVALID` if a message gets
|
||||
# deleted or `WORKER_BUSY_TOO_LONG_RETRY` if there are issues at
|
||||
# Telegram), in which case we get some "missing" message mappings.
|
||||
# Log them with the hope that we can better work around them.
|
||||
#
|
||||
# This also happens when trying to forward messages that can't
|
||||
# be forwarded because they don't exist (0, service, deleted)
|
||||
# among others which could be (like deleted or existing).
|
||||
self._log[__name__].warning(
|
||||
'Request %s had missing message mappings %s', request, result)
|
||||
|
||||
return [
|
||||
id_to_message.get(random_to_id[rnd])
|
||||
if rnd in random_to_id
|
||||
else None
|
||||
for rnd in random_id
|
||||
]
|
||||
|
||||
# endregion
|
File diff suppressed because it is too large
Load Diff
|
@ -1,951 +0,0 @@
|
|||
import abc
|
||||
import re
|
||||
import asyncio
|
||||
import collections
|
||||
import logging
|
||||
import platform
|
||||
import time
|
||||
import typing
|
||||
import datetime
|
||||
|
||||
from .. import version, helpers, __name__ as __base_name__
|
||||
from ..crypto import rsa
|
||||
from ..extensions import markdown
|
||||
from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy
|
||||
from ..sessions import Session, SQLiteSession, MemorySession
|
||||
from ..tl import functions, types
|
||||
from ..tl.alltlobjects import LAYER
|
||||
from .._updates import MessageBox, EntityCache as MbEntityCache, SessionState, ChannelState, Entity, EntityType
|
||||
|
||||
DEFAULT_DC_ID = 2
|
||||
DEFAULT_IPV4_IP = '149.154.167.51'
|
||||
DEFAULT_IPV6_IP = '2001:67c:4e8:f002::a'
|
||||
DEFAULT_PORT = 443
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
||||
_base_log = logging.getLogger(__base_name__)
|
||||
|
||||
|
||||
# In seconds, how long to wait before disconnecting a exported sender.
|
||||
_DISCONNECT_EXPORTED_AFTER = 60
|
||||
|
||||
|
||||
class _ExportState:
|
||||
def __init__(self):
|
||||
# ``n`` is the amount of borrows a given sender has;
|
||||
# once ``n`` reaches ``0``, disconnect the sender after a while.
|
||||
self._n = 0
|
||||
self._zero_ts = 0
|
||||
self._connected = False
|
||||
|
||||
def add_borrow(self):
|
||||
self._n += 1
|
||||
self._connected = True
|
||||
|
||||
def add_return(self):
|
||||
self._n -= 1
|
||||
assert self._n >= 0, 'returned sender more than it was borrowed'
|
||||
if self._n == 0:
|
||||
self._zero_ts = time.time()
|
||||
|
||||
def should_disconnect(self):
|
||||
return (self._n == 0
|
||||
and self._connected
|
||||
and (time.time() - self._zero_ts) > _DISCONNECT_EXPORTED_AFTER)
|
||||
|
||||
def need_connect(self):
|
||||
return not self._connected
|
||||
|
||||
def mark_disconnected(self):
|
||||
assert self.should_disconnect(), 'marked as disconnected when it was borrowed'
|
||||
self._connected = False
|
||||
|
||||
|
||||
# TODO How hard would it be to support both `trio` and `asyncio`?
|
||||
class TelegramBaseClient(abc.ABC):
|
||||
"""
|
||||
This is the abstract base class for the client. It defines some
|
||||
basic stuff like connecting, switching data center, etc, and
|
||||
leaves the `__call__` unimplemented.
|
||||
|
||||
Arguments
|
||||
session (`str` | `telethon.sessions.abstract.Session`, `None`):
|
||||
The file name of the session file to be used if a string is
|
||||
given (it may be a full path), or the Session instance to be
|
||||
used otherwise. If it's `None`, the session will not be saved,
|
||||
and you should call :meth:`.log_out()` when you're done.
|
||||
|
||||
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.
|
||||
|
||||
api_id (`int` | `str`):
|
||||
The API ID you obtained from https://my.telegram.org.
|
||||
|
||||
api_hash (`str`):
|
||||
The API hash you obtained from https://my.telegram.org.
|
||||
|
||||
connection (`telethon.network.connection.common.Connection`, optional):
|
||||
The connection instance to be used when creating a new connection
|
||||
to the servers. It **must** be a type.
|
||||
|
||||
Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`.
|
||||
|
||||
use_ipv6 (`bool`, optional):
|
||||
Whether to connect to the servers through IPv6 or not.
|
||||
By default this is `False` as IPv6 support is not
|
||||
too widespread yet.
|
||||
|
||||
proxy (`tuple` | `list` | `dict`, optional):
|
||||
An iterable consisting of the proxy info. If `connection` is
|
||||
one of `MTProxy`, then it should contain MTProxy credentials:
|
||||
``('hostname', port, 'secret')``. Otherwise, it's meant to store
|
||||
function parameters for PySocks, like ``(type, 'hostname', port)``.
|
||||
See https://github.com/Anorov/PySocks#usage-1 for more.
|
||||
|
||||
local_addr (`str` | `tuple`, optional):
|
||||
Local host address (and port, optionally) used to bind the socket to locally.
|
||||
You only need to use this if you have multiple network cards and
|
||||
want to use a specific one.
|
||||
|
||||
timeout (`int` | `float`, optional):
|
||||
The timeout in seconds to be used when connecting.
|
||||
This is **not** the timeout to be used when ``await``'ing for
|
||||
invoked requests, and you should use ``asyncio.wait`` or
|
||||
``asyncio.wait_for`` for that.
|
||||
|
||||
request_retries (`int` | `None`, optional):
|
||||
How many times a request should be retried. Request are retried
|
||||
when Telegram is having internal issues (due to either
|
||||
``errors.ServerError`` or ``errors.RpcCallFailError``),
|
||||
when there is a ``errors.FloodWaitError`` less than
|
||||
`flood_sleep_threshold`, or when there's a migrate error.
|
||||
|
||||
May take a negative or `None` value for infinite retries, but
|
||||
this is not recommended, since some requests can always trigger
|
||||
a call fail (such as searching for messages).
|
||||
|
||||
connection_retries (`int` | `None`, optional):
|
||||
How many times the reconnection should retry, either on the
|
||||
initial connection or when Telegram disconnects us. May be
|
||||
set to a negative or `None` value for infinite retries, but
|
||||
this is not recommended, since the program can get stuck in an
|
||||
infinite loop.
|
||||
|
||||
retry_delay (`int` | `float`, optional):
|
||||
The delay in seconds to sleep between automatic reconnections.
|
||||
|
||||
auto_reconnect (`bool`, optional):
|
||||
Whether reconnection should be retried `connection_retries`
|
||||
times automatically if Telegram disconnects us or not.
|
||||
|
||||
sequential_updates (`bool`, optional):
|
||||
By default every incoming update will create a new task, so
|
||||
you can handle several updates in parallel. Some scripts need
|
||||
the order in which updates are processed to be sequential, and
|
||||
this setting allows them to do so.
|
||||
|
||||
If set to `True`, incoming updates will be put in a queue
|
||||
and processed sequentially. This means your event handlers
|
||||
should *not* perform long-running operations since new
|
||||
updates are put inside of an unbounded queue.
|
||||
|
||||
flood_sleep_threshold (`int` | `float`, optional):
|
||||
The threshold below which the library should automatically
|
||||
sleep on flood wait and slow mode wait errors (inclusive). For instance, if a
|
||||
``FloodWaitError`` for 17s occurs and `flood_sleep_threshold`
|
||||
is 20s, the library will ``sleep`` automatically. If the error
|
||||
was for 21s, it would ``raise FloodWaitError`` instead. Values
|
||||
larger than a day (like ``float('inf')``) will be changed to a day.
|
||||
|
||||
raise_last_call_error (`bool`, optional):
|
||||
When API calls fail in a way that causes Telethon to retry
|
||||
automatically, should the RPC error of the last attempt be raised
|
||||
instead of a generic ValueError. This is mostly useful for
|
||||
detecting when Telegram has internal issues.
|
||||
|
||||
device_model (`str`, optional):
|
||||
"Device model" to be sent when creating the initial connection.
|
||||
Defaults to 'PC (n)bit' derived from ``platform.uname().machine``, or its direct value if unknown.
|
||||
|
||||
system_version (`str`, optional):
|
||||
"System version" to be sent when creating the initial connection.
|
||||
Defaults to ``platform.uname().release`` stripped of everything ahead of -.
|
||||
|
||||
app_version (`str`, optional):
|
||||
"App version" to be sent when creating the initial connection.
|
||||
Defaults to `telethon.version.__version__`.
|
||||
|
||||
lang_code (`str`, optional):
|
||||
"Language code" to be sent when creating the initial connection.
|
||||
Defaults to ``'en'``.
|
||||
|
||||
system_lang_code (`str`, optional):
|
||||
"System lang code" to be sent when creating the initial connection.
|
||||
Defaults to `lang_code`.
|
||||
|
||||
loop (`asyncio.AbstractEventLoop`, optional):
|
||||
Asyncio event loop to use. Defaults to `asyncio.get_running_loop()`.
|
||||
This argument is ignored.
|
||||
|
||||
base_logger (`str` | `logging.Logger`, optional):
|
||||
Base logger name or instance to use.
|
||||
If a `str` is given, it'll be passed to `logging.getLogger()`. If a
|
||||
`logging.Logger` is given, it'll be used directly. If something
|
||||
else or nothing is given, the default logger will be used.
|
||||
|
||||
receive_updates (`bool`, optional):
|
||||
Whether the client will receive updates or not. By default, updates
|
||||
will be received from Telegram as they occur.
|
||||
|
||||
Turning this off means that Telegram will not send updates at all
|
||||
so event handlers, conversations, and QR login will not work.
|
||||
However, certain scripts don't need updates, so this will reduce
|
||||
the amount of bandwidth used.
|
||||
|
||||
entity_cache_limit (`int`, optional):
|
||||
How many users, chats and channels to keep in the in-memory cache
|
||||
at most. This limit is checked against when processing updates.
|
||||
|
||||
When this limit is reached or exceeded, all entities that are not
|
||||
required for update handling will be flushed to the session file.
|
||||
|
||||
Note that this implies that there is a lower bound to the amount
|
||||
of entities that must be kept in memory.
|
||||
|
||||
Setting this limit too low will cause the library to attempt to
|
||||
flush entities to the session file even if no entities can be
|
||||
removed from the in-memory cache, which will degrade performance.
|
||||
"""
|
||||
|
||||
# Current TelegramClient version
|
||||
__version__ = version.__version__
|
||||
|
||||
# Cached server configuration (with .dc_options), can be "global"
|
||||
_config = None
|
||||
_cdn_config = None
|
||||
|
||||
# region Initialization
|
||||
|
||||
def __init__(
|
||||
self: 'TelegramClient',
|
||||
session: 'typing.Union[str, Session]',
|
||||
api_id: int,
|
||||
api_hash: str,
|
||||
*,
|
||||
connection: 'typing.Type[Connection]' = ConnectionTcpFull,
|
||||
use_ipv6: bool = False,
|
||||
proxy: typing.Union[tuple, dict] = None,
|
||||
local_addr: typing.Union[str, tuple] = None,
|
||||
timeout: int = 10,
|
||||
request_retries: int = 5,
|
||||
connection_retries: int = 5,
|
||||
retry_delay: int = 1,
|
||||
auto_reconnect: bool = True,
|
||||
sequential_updates: bool = False,
|
||||
flood_sleep_threshold: int = 60,
|
||||
raise_last_call_error: bool = False,
|
||||
device_model: str = None,
|
||||
system_version: str = None,
|
||||
app_version: str = None,
|
||||
lang_code: str = 'en',
|
||||
system_lang_code: str = 'en',
|
||||
loop: asyncio.AbstractEventLoop = None,
|
||||
base_logger: typing.Union[str, logging.Logger] = None,
|
||||
receive_updates: bool = True,
|
||||
catch_up: bool = False,
|
||||
entity_cache_limit: int = 5000
|
||||
):
|
||||
if not api_id or not api_hash:
|
||||
raise ValueError(
|
||||
"Your API ID or Hash cannot be empty or None. "
|
||||
"Refer to telethon.rtfd.io for more information.")
|
||||
|
||||
self._use_ipv6 = use_ipv6
|
||||
|
||||
if isinstance(base_logger, str):
|
||||
base_logger = logging.getLogger(base_logger)
|
||||
elif not isinstance(base_logger, logging.Logger):
|
||||
base_logger = _base_log
|
||||
|
||||
class _Loggers(dict):
|
||||
def __missing__(self, key):
|
||||
if key.startswith("telethon."):
|
||||
key = key.split('.', maxsplit=1)[1]
|
||||
|
||||
return base_logger.getChild(key)
|
||||
|
||||
self._log = _Loggers()
|
||||
|
||||
# Determine what session object we have
|
||||
if isinstance(session, str) or session is None:
|
||||
try:
|
||||
session = SQLiteSession(session)
|
||||
except ImportError:
|
||||
import warnings
|
||||
warnings.warn(
|
||||
'The sqlite3 module is not available under this '
|
||||
'Python installation and no custom session '
|
||||
'instance was given; using MemorySession.\n'
|
||||
'You will need to re-login every time unless '
|
||||
'you use another session storage'
|
||||
)
|
||||
session = MemorySession()
|
||||
elif not isinstance(session, Session):
|
||||
raise TypeError(
|
||||
'The given session must be a str or a Session instance.'
|
||||
)
|
||||
|
||||
# ':' in session.server_address is True if it's an IPv6 address
|
||||
if (not session.server_address or
|
||||
(':' in session.server_address) != use_ipv6):
|
||||
session.set_dc(
|
||||
DEFAULT_DC_ID,
|
||||
DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP,
|
||||
DEFAULT_PORT
|
||||
)
|
||||
|
||||
self.flood_sleep_threshold = flood_sleep_threshold
|
||||
|
||||
# TODO Use AsyncClassWrapper(session)
|
||||
# ChatGetter and SenderGetter can use the in-memory _mb_entity_cache
|
||||
# to avoid network access and the need for await in session files.
|
||||
#
|
||||
# The session files only wants the entities to persist
|
||||
# them to disk, and to save additional useful information.
|
||||
# TODO Session should probably return all cached
|
||||
# info of entities, not just the input versions
|
||||
self.session = session
|
||||
self.api_id = int(api_id)
|
||||
self.api_hash = api_hash
|
||||
|
||||
# Current proxy implementation requires `sock_connect`, and some
|
||||
# event loops lack this method. If the current loop is missing it,
|
||||
# bail out early and suggest an alternative.
|
||||
#
|
||||
# TODO A better fix is obviously avoiding the use of `sock_connect`
|
||||
#
|
||||
# See https://github.com/LonamiWebs/Telethon/issues/1337 for details.
|
||||
if not callable(getattr(self.loop, 'sock_connect', None)):
|
||||
raise TypeError(
|
||||
'Event loop of type {} lacks `sock_connect`, which is needed to use proxies.\n\n'
|
||||
'Change the event loop in use to use proxies:\n'
|
||||
'# https://github.com/LonamiWebs/Telethon/issues/1337\n'
|
||||
'import asyncio\n'
|
||||
'asyncio.set_event_loop(asyncio.SelectorEventLoop())'.format(
|
||||
self.loop.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
if local_addr is not None:
|
||||
if use_ipv6 is False and ':' in local_addr:
|
||||
raise TypeError(
|
||||
'A local IPv6 address must only be used with `use_ipv6=True`.'
|
||||
)
|
||||
elif use_ipv6 is True and ':' not in local_addr:
|
||||
raise TypeError(
|
||||
'`use_ipv6=True` must only be used with a local IPv6 address.'
|
||||
)
|
||||
|
||||
self._raise_last_call_error = raise_last_call_error
|
||||
|
||||
self._request_retries = request_retries
|
||||
self._connection_retries = connection_retries
|
||||
self._retry_delay = retry_delay or 0
|
||||
self._proxy = proxy
|
||||
self._local_addr = local_addr
|
||||
self._timeout = timeout
|
||||
self._auto_reconnect = auto_reconnect
|
||||
|
||||
assert isinstance(connection, type)
|
||||
self._connection = connection
|
||||
init_proxy = None if not issubclass(connection, TcpMTProxy) else \
|
||||
types.InputClientProxy(*connection.address_info(proxy))
|
||||
|
||||
# Used on connection. Capture the variables in a lambda since
|
||||
# exporting clients need to create this InvokeWithLayerRequest.
|
||||
system = platform.uname()
|
||||
|
||||
if system.machine in ('x86_64', 'AMD64'):
|
||||
default_device_model = 'PC 64bit'
|
||||
elif system.machine in ('i386','i686','x86'):
|
||||
default_device_model = 'PC 32bit'
|
||||
else:
|
||||
default_device_model = system.machine
|
||||
default_system_version = re.sub(r'-.+','',system.release)
|
||||
|
||||
self._init_request = functions.InitConnectionRequest(
|
||||
api_id=self.api_id,
|
||||
device_model=device_model or default_device_model or 'Unknown',
|
||||
system_version=system_version or default_system_version or '1.0',
|
||||
app_version=app_version or self.__version__,
|
||||
lang_code=lang_code,
|
||||
system_lang_code=system_lang_code,
|
||||
lang_pack='', # "langPacks are for official apps only"
|
||||
query=None,
|
||||
proxy=init_proxy
|
||||
)
|
||||
|
||||
# Remember flood-waited requests to avoid making them again
|
||||
self._flood_waited_requests = {}
|
||||
|
||||
# Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders
|
||||
self._borrowed_senders = {}
|
||||
self._borrow_sender_lock = asyncio.Lock()
|
||||
|
||||
self._loop = None # only used as a sanity check
|
||||
self._updates_error = None
|
||||
self._updates_handle = None
|
||||
self._keepalive_handle = None
|
||||
self._last_request = time.time()
|
||||
self._no_updates = not receive_updates
|
||||
|
||||
# Used for non-sequential updates, in order to terminate all pending tasks on disconnect.
|
||||
self._sequential_updates = sequential_updates
|
||||
self._event_handler_tasks = set()
|
||||
|
||||
self._authorized = None # None = unknown, False = no, True = yes
|
||||
|
||||
# Some further state for subclasses
|
||||
self._event_builders = []
|
||||
|
||||
# {chat_id: {Conversation}}
|
||||
self._conversations = collections.defaultdict(set)
|
||||
|
||||
# Hack to workaround the fact Telegram may send album updates as
|
||||
# different Updates when being sent from a different data center.
|
||||
# {grouped_id: AlbumHack}
|
||||
#
|
||||
# FIXME: We don't bother cleaning this up because it's not really
|
||||
# worth it, albums are pretty rare and this only holds them
|
||||
# for a second at most.
|
||||
self._albums = {}
|
||||
|
||||
# Default parse mode
|
||||
self._parse_mode = markdown
|
||||
|
||||
# Some fields to easy signing in. Let {phone: hash} be
|
||||
# a dictionary because the user may change their mind.
|
||||
self._phone_code_hash = {}
|
||||
self._phone = None
|
||||
self._tos = None
|
||||
|
||||
# A place to store if channels are a megagroup or not (see `edit_admin`)
|
||||
self._megagroup_cache = {}
|
||||
|
||||
# This is backported from v2 in a very ad-hoc way just to get proper update handling
|
||||
self._catch_up = catch_up
|
||||
self._updates_queue = asyncio.Queue()
|
||||
self._message_box = MessageBox(self._log['messagebox'])
|
||||
self._mb_entity_cache = MbEntityCache() # required for proper update handling (to know when to getDifference)
|
||||
self._entity_cache_limit = entity_cache_limit
|
||||
|
||||
self._sender = MTProtoSender(
|
||||
self.session.auth_key,
|
||||
loggers=self._log,
|
||||
retries=self._connection_retries,
|
||||
delay=self._retry_delay,
|
||||
auto_reconnect=self._auto_reconnect,
|
||||
connect_timeout=self._timeout,
|
||||
auth_key_callback=self._auth_key_callback,
|
||||
updates_queue=self._updates_queue,
|
||||
auto_reconnect_callback=self._handle_auto_reconnect
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Properties
|
||||
|
||||
@property
|
||||
def loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop:
|
||||
"""
|
||||
Property with the ``asyncio`` event loop used by this client.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Download media in the background
|
||||
task = client.loop.create_task(message.download_media())
|
||||
|
||||
# Do some work
|
||||
...
|
||||
|
||||
# Join the task (wait for it to complete)
|
||||
await task
|
||||
"""
|
||||
return helpers.get_running_loop()
|
||||
|
||||
@property
|
||||
def disconnected(self: 'TelegramClient') -> asyncio.Future:
|
||||
"""
|
||||
Property with a ``Future`` that resolves upon disconnection.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Wait for a disconnection to occur
|
||||
try:
|
||||
await client.disconnected
|
||||
except OSError:
|
||||
print('Error on disconnect')
|
||||
"""
|
||||
return self._sender.disconnected
|
||||
|
||||
@property
|
||||
def flood_sleep_threshold(self):
|
||||
return self._flood_sleep_threshold
|
||||
|
||||
@flood_sleep_threshold.setter
|
||||
def flood_sleep_threshold(self, value):
|
||||
# None -> 0, negative values don't really matter
|
||||
self._flood_sleep_threshold = min(value or 0, 24 * 60 * 60)
|
||||
|
||||
# endregion
|
||||
|
||||
# region Connecting
|
||||
|
||||
async def connect(self: 'TelegramClient') -> None:
|
||||
"""
|
||||
Connects to Telegram.
|
||||
|
||||
.. note::
|
||||
|
||||
Connect means connect and nothing else, and only one low-level
|
||||
request is made to notify Telegram about which layer we will be
|
||||
using.
|
||||
|
||||
Before Telegram sends you updates, you need to make a high-level
|
||||
request, like `client.get_me() <telethon.client.users.UserMethods.get_me>`,
|
||||
as described in https://core.telegram.org/api/updates.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
try:
|
||||
await client.connect()
|
||||
except OSError:
|
||||
print('Failed to connect')
|
||||
"""
|
||||
if self.session is None:
|
||||
raise ValueError('TelegramClient instance cannot be reused after logging out')
|
||||
|
||||
if self._loop is None:
|
||||
self._loop = helpers.get_running_loop()
|
||||
elif self._loop != helpers.get_running_loop():
|
||||
raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)')
|
||||
|
||||
if not await self._sender.connect(self._connection(
|
||||
self.session.server_address,
|
||||
self.session.port,
|
||||
self.session.dc_id,
|
||||
loggers=self._log,
|
||||
proxy=self._proxy,
|
||||
local_addr=self._local_addr
|
||||
)):
|
||||
# We don't want to init or modify anything if we were already connected
|
||||
return
|
||||
|
||||
self.session.auth_key = self._sender.auth_key
|
||||
self.session.save()
|
||||
|
||||
try:
|
||||
# See comment when saving entities to understand this hack
|
||||
self_id = self.session.get_input_entity(0).access_hash
|
||||
self_user = self.session.get_input_entity(self_id)
|
||||
self._mb_entity_cache.set_self_user(self_id, None, self_user.access_hash)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if self._catch_up:
|
||||
ss = SessionState(0, 0, False, 0, 0, 0, 0, None)
|
||||
cs = []
|
||||
|
||||
for entity_id, state in self.session.get_update_states():
|
||||
if entity_id == 0:
|
||||
# TODO current session doesn't store self-user info but adding that is breaking on downstream session impls
|
||||
ss = SessionState(0, 0, False, state.pts, state.qts, int(state.date.timestamp()), state.seq, None)
|
||||
else:
|
||||
cs.append(ChannelState(entity_id, state.pts))
|
||||
|
||||
self._message_box.load(ss, cs)
|
||||
for state in cs:
|
||||
try:
|
||||
entity = self.session.get_input_entity(state.channel_id)
|
||||
except ValueError:
|
||||
self._log[__name__].warning(
|
||||
'No access_hash in cache for channel %s, will not catch up', state.channel_id)
|
||||
else:
|
||||
self._mb_entity_cache.put(Entity(EntityType.CHANNEL, entity.channel_id, entity.access_hash))
|
||||
|
||||
self._init_request.query = functions.help.GetConfigRequest()
|
||||
|
||||
req = self._init_request
|
||||
if self._no_updates:
|
||||
req = functions.InvokeWithoutUpdatesRequest(req)
|
||||
|
||||
await self._sender.send(functions.InvokeWithLayerRequest(LAYER, req))
|
||||
|
||||
if self._message_box.is_empty():
|
||||
me = await self.get_me()
|
||||
if me:
|
||||
await self._on_login(me) # also calls GetState to initialize the MessageBox
|
||||
|
||||
self._updates_handle = self.loop.create_task(self._update_loop())
|
||||
self._keepalive_handle = self.loop.create_task(self._keepalive_loop())
|
||||
|
||||
def is_connected(self: 'TelegramClient') -> bool:
|
||||
"""
|
||||
Returns `True` if the user has connected.
|
||||
|
||||
This method is **not** asynchronous (don't use ``await`` on it).
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
while client.is_connected():
|
||||
await asyncio.sleep(1)
|
||||
"""
|
||||
sender = getattr(self, '_sender', None)
|
||||
return sender and sender.is_connected()
|
||||
|
||||
def disconnect(self: 'TelegramClient'):
|
||||
"""
|
||||
Disconnects from Telegram.
|
||||
|
||||
If the event loop is already running, this method returns a
|
||||
coroutine that you should await on your own code; otherwise
|
||||
the loop is ran until said coroutine completes.
|
||||
|
||||
Event handlers which are currently running will be cancelled before
|
||||
this function returns (in order to properly clean-up their tasks).
|
||||
In particular, this means that using ``disconnect`` in a handler
|
||||
will cause code after the ``disconnect`` to never run. If this is
|
||||
needed, consider spawning a separate task to do the remaining work.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# You don't need to use this if you used "with client"
|
||||
await client.disconnect()
|
||||
"""
|
||||
if self.loop.is_running():
|
||||
# Disconnect may be called from an event handler, which would
|
||||
# cancel itself during itself and never actually complete the
|
||||
# disconnection. Shield the task to prevent disconnect itself
|
||||
# from being cancelled. See issue #3942 for more details.
|
||||
return asyncio.shield(self.loop.create_task(self._disconnect_coro()))
|
||||
else:
|
||||
try:
|
||||
self.loop.run_until_complete(self._disconnect_coro())
|
||||
except RuntimeError:
|
||||
# Python 3.5.x complains when called from
|
||||
# `__aexit__` and there were pending updates with:
|
||||
# "Event loop stopped before Future completed."
|
||||
#
|
||||
# However, it doesn't really make a lot of sense.
|
||||
pass
|
||||
|
||||
def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]):
|
||||
"""
|
||||
Changes the proxy which will be used on next (re)connection.
|
||||
|
||||
Method has no immediate effects if the client is currently connected.
|
||||
|
||||
The new proxy will take it's effect on the next reconnection attempt:
|
||||
- on a call `await client.connect()` (after complete disconnect)
|
||||
- on auto-reconnect attempt (e.g, after previous connection was lost)
|
||||
"""
|
||||
init_proxy = None if not issubclass(self._connection, TcpMTProxy) else \
|
||||
types.InputClientProxy(*self._connection.address_info(proxy))
|
||||
|
||||
self._init_request.proxy = init_proxy
|
||||
self._proxy = proxy
|
||||
|
||||
# While `await client.connect()` passes new proxy on each new call,
|
||||
# auto-reconnect attempts use already set up `_connection` inside
|
||||
# the `_sender`, so the only way to change proxy between those
|
||||
# is to directly inject parameters.
|
||||
|
||||
connection = getattr(self._sender, "_connection", None)
|
||||
if connection:
|
||||
if isinstance(connection, TcpMTProxy):
|
||||
connection._ip = proxy[0]
|
||||
connection._port = proxy[1]
|
||||
else:
|
||||
connection._proxy = proxy
|
||||
|
||||
def _save_states_and_entities(self: 'TelegramClient'):
|
||||
entities = self._mb_entity_cache.get_all_entities()
|
||||
|
||||
# Piggy-back on an arbitrary TL type with users and chats so the session can understand to read the entities.
|
||||
# It doesn't matter if we put users in the list of chats.
|
||||
self.session.process_entities(types.contacts.ResolvedPeer(None, [e._as_input_peer() for e in entities], []))
|
||||
|
||||
# As a hack to not need to change the session files, save ourselves with ``id=0`` and ``access_hash`` of our ``id``.
|
||||
# This way it is possible to determine our own ID by querying for 0. However, whether we're a bot is not saved.
|
||||
if self._mb_entity_cache.self_id:
|
||||
self.session.process_entities(types.contacts.ResolvedPeer(None, [types.InputPeerUser(0, self._mb_entity_cache.self_id)], []))
|
||||
|
||||
ss, cs = self._message_box.session_state()
|
||||
self.session.set_update_state(0, types.updates.State(**ss, unread_count=0))
|
||||
now = datetime.datetime.now() # any datetime works; channels don't need it
|
||||
for channel_id, pts in cs.items():
|
||||
self.session.set_update_state(channel_id, types.updates.State(pts, 0, now, 0, unread_count=0))
|
||||
|
||||
async def _disconnect_coro(self: 'TelegramClient'):
|
||||
if self.session is None:
|
||||
return # already logged out and disconnected
|
||||
|
||||
await self._disconnect()
|
||||
|
||||
# Also clean-up all exported senders because we're done with them
|
||||
async with self._borrow_sender_lock:
|
||||
for state, sender in self._borrowed_senders.values():
|
||||
# Note that we're not checking for `state.should_disconnect()`.
|
||||
# If the user wants to disconnect the client, ALL connections
|
||||
# to Telegram (including exported senders) should be closed.
|
||||
#
|
||||
# Disconnect should never raise, so there's no try/except.
|
||||
await sender.disconnect()
|
||||
# Can't use `mark_disconnected` because it may be borrowed.
|
||||
state._connected = False
|
||||
|
||||
# If any was borrowed
|
||||
self._borrowed_senders.clear()
|
||||
|
||||
# trio's nurseries would handle this for us, but this is asyncio.
|
||||
# All tasks spawned in the background should properly be terminated.
|
||||
if self._event_handler_tasks:
|
||||
for task in self._event_handler_tasks:
|
||||
task.cancel()
|
||||
|
||||
await asyncio.wait(self._event_handler_tasks)
|
||||
self._event_handler_tasks.clear()
|
||||
|
||||
self._save_states_and_entities()
|
||||
|
||||
self.session.close()
|
||||
|
||||
async def _disconnect(self: 'TelegramClient'):
|
||||
"""
|
||||
Disconnect only, without closing the session. Used in reconnections
|
||||
to different data centers, where we don't want to close the session
|
||||
file; user disconnects however should close it since it means that
|
||||
their job with the client is complete and we should clean it up all.
|
||||
"""
|
||||
await self._sender.disconnect()
|
||||
await helpers._cancel(self._log[__name__],
|
||||
updates_handle=self._updates_handle,
|
||||
keepalive_handle=self._keepalive_handle)
|
||||
|
||||
async def _switch_dc(self: 'TelegramClient', new_dc):
|
||||
"""
|
||||
Permanently switches the current connection to the new data center.
|
||||
"""
|
||||
self._log[__name__].info('Reconnecting to new data center %s', new_dc)
|
||||
dc = await 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._sender.auth_key.key = None
|
||||
self.session.auth_key = None
|
||||
self.session.save()
|
||||
await self._disconnect()
|
||||
return await self.connect()
|
||||
|
||||
def _auth_key_callback(self: 'TelegramClient', auth_key):
|
||||
"""
|
||||
Callback from the sender whenever it needed to generate a
|
||||
new authorization key. This means we are not authorized.
|
||||
"""
|
||||
self.session.auth_key = auth_key
|
||||
self.session.save()
|
||||
|
||||
# endregion
|
||||
|
||||
# region Working with different connections/Data Centers
|
||||
|
||||
async def _get_dc(self: 'TelegramClient', 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())
|
||||
|
||||
if cdn and not self._cdn_config:
|
||||
cls._cdn_config = await self(functions.help.GetCdnConfigRequest())
|
||||
for pk in cls._cdn_config.public_keys:
|
||||
rsa.add_key(pk.public_key)
|
||||
|
||||
try:
|
||||
return next(
|
||||
dc for dc in cls._config.dc_options
|
||||
if dc.id == dc_id
|
||||
and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn
|
||||
)
|
||||
except StopIteration:
|
||||
self._log[__name__].warning(
|
||||
'Failed to get DC %s (cdn = %s) with use_ipv6 = %s; retrying ignoring IPv6 check',
|
||||
dc_id, cdn, self._use_ipv6
|
||||
)
|
||||
return next(
|
||||
dc for dc in cls._config.dc_options
|
||||
if dc.id == dc_id and bool(dc.cdn) == cdn
|
||||
)
|
||||
|
||||
async def _create_exported_sender(self: 'TelegramClient', dc_id):
|
||||
"""
|
||||
Creates a new exported `MTProtoSender` for the given `dc_id` and
|
||||
returns it. This method should be used by `_borrow_exported_sender`.
|
||||
"""
|
||||
# Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt
|
||||
# for clearly showing how to export the authorization
|
||||
dc = await self._get_dc(dc_id)
|
||||
# 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(None, loggers=self._log)
|
||||
await sender.connect(self._connection(
|
||||
dc.ip_address,
|
||||
dc.port,
|
||||
dc.id,
|
||||
loggers=self._log,
|
||||
proxy=self._proxy,
|
||||
local_addr=self._local_addr
|
||||
))
|
||||
self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc)
|
||||
auth = await self(functions.auth.ExportAuthorizationRequest(dc_id))
|
||||
self._init_request.query = functions.auth.ImportAuthorizationRequest(id=auth.id, bytes=auth.bytes)
|
||||
req = functions.InvokeWithLayerRequest(LAYER, self._init_request)
|
||||
await sender.send(req)
|
||||
return sender
|
||||
|
||||
async def _borrow_exported_sender(self: 'TelegramClient', dc_id):
|
||||
"""
|
||||
Borrows a connected `MTProtoSender` for the given `dc_id`.
|
||||
If it's not cached, creates a new one if it doesn't exist yet,
|
||||
and imports a freshly exported authorization key for it to be usable.
|
||||
|
||||
Once its job is over it should be `_return_exported_sender`.
|
||||
"""
|
||||
async with self._borrow_sender_lock:
|
||||
self._log[__name__].debug('Borrowing sender for dc_id %d', dc_id)
|
||||
state, sender = self._borrowed_senders.get(dc_id, (None, None))
|
||||
|
||||
if state is None:
|
||||
state = _ExportState()
|
||||
sender = await self._create_exported_sender(dc_id)
|
||||
sender.dc_id = dc_id
|
||||
self._borrowed_senders[dc_id] = (state, sender)
|
||||
|
||||
elif state.need_connect():
|
||||
dc = await self._get_dc(dc_id)
|
||||
await sender.connect(self._connection(
|
||||
dc.ip_address,
|
||||
dc.port,
|
||||
dc.id,
|
||||
loggers=self._log,
|
||||
proxy=self._proxy,
|
||||
local_addr=self._local_addr
|
||||
))
|
||||
|
||||
state.add_borrow()
|
||||
return sender
|
||||
|
||||
async def _return_exported_sender(self: 'TelegramClient', sender):
|
||||
"""
|
||||
Returns a borrowed exported sender. If all borrows have
|
||||
been returned, the sender is cleanly disconnected.
|
||||
"""
|
||||
async with self._borrow_sender_lock:
|
||||
self._log[__name__].debug('Returning borrowed sender for dc_id %d', sender.dc_id)
|
||||
state, _ = self._borrowed_senders[sender.dc_id]
|
||||
state.add_return()
|
||||
|
||||
async def _clean_exported_senders(self: 'TelegramClient'):
|
||||
"""
|
||||
Cleans-up all unused exported senders by disconnecting them.
|
||||
"""
|
||||
async with self._borrow_sender_lock:
|
||||
for dc_id, (state, sender) in self._borrowed_senders.items():
|
||||
if state.should_disconnect():
|
||||
self._log[__name__].info(
|
||||
'Disconnecting borrowed sender for DC %d', dc_id)
|
||||
|
||||
# Disconnect should never raise
|
||||
await sender.disconnect()
|
||||
state.mark_disconnected()
|
||||
|
||||
async def _get_cdn_client(self: 'TelegramClient', cdn_redirect):
|
||||
"""Similar to ._borrow_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)
|
||||
session = self.session.clone()
|
||||
session.set_dc(dc.id, dc.ip_address, dc.port)
|
||||
self._exported_sessions[cdn_redirect.dc_id] = session
|
||||
|
||||
self._log[__name__].info('Creating new CDN client')
|
||||
client = TelegramBaseClient(
|
||||
session, self.api_id, self.api_hash,
|
||||
proxy=self._sender.connection.conn.proxy,
|
||||
timeout=self._sender.connection.get_timeout()
|
||||
)
|
||||
|
||||
# This will make use of the new RSA keys for this specific CDN.
|
||||
#
|
||||
# We won't be calling GetConfigRequest because it's only called
|
||||
# when needed by ._get_dc, and also it's static so it's likely
|
||||
# set already. Avoid invoking non-CDN methods by not syncing updates.
|
||||
client.connect(_sync_updates=False)
|
||||
return client
|
||||
|
||||
# endregion
|
||||
|
||||
# region Invoking Telegram requests
|
||||
|
||||
@abc.abstractmethod
|
||||
def __call__(self: 'TelegramClient', request, ordered=False):
|
||||
"""
|
||||
Invokes (sends) one or more MTProtoRequests and returns (receives)
|
||||
their result.
|
||||
|
||||
Args:
|
||||
request (`TLObject` | `list`):
|
||||
The request or requests to be invoked.
|
||||
|
||||
ordered (`bool`, optional):
|
||||
Whether the requests (if more than one was given) should be
|
||||
executed sequentially on the server. They run in arbitrary
|
||||
order by default.
|
||||
|
||||
flood_sleep_threshold (`int` | `None`, optional):
|
||||
The flood sleep threshold to use for this request. This overrides
|
||||
the default value stored in
|
||||
`client.flood_sleep_threshold <telethon.client.telegrambaseclient.TelegramBaseClient.flood_sleep_threshold>`
|
||||
|
||||
Returns:
|
||||
The result of the request (often a `TLObject`) or a list of
|
||||
results if more than one request was given.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def _update_loop(self: 'TelegramClient'):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _handle_auto_reconnect(self: 'TelegramClient'):
|
||||
raise NotImplementedError
|
||||
|
||||
# endregion
|
|
@ -1,13 +0,0 @@
|
|||
from . import (
|
||||
AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods,
|
||||
BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods,
|
||||
MessageParseMethods, UserMethods, TelegramBaseClient
|
||||
)
|
||||
|
||||
|
||||
class TelegramClient(
|
||||
AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods,
|
||||
BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods,
|
||||
MessageParseMethods, UserMethods, TelegramBaseClient
|
||||
):
|
||||
pass
|
|
@ -1,691 +0,0 @@
|
|||
import asyncio
|
||||
import inspect
|
||||
import itertools
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import typing
|
||||
import logging
|
||||
import warnings
|
||||
from collections import deque
|
||||
|
||||
from .. import events, utils, errors
|
||||
from ..events.common import EventBuilder, EventCommon
|
||||
from ..tl import types, functions
|
||||
from .._updates import GapError, PrematureEndReason
|
||||
from ..helpers import get_running_loop
|
||||
from ..version import __version__
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
||||
|
||||
Callback = typing.Callable[[typing.Any], typing.Any]
|
||||
|
||||
class UpdateMethods:
|
||||
|
||||
# region Public methods
|
||||
|
||||
async def _run_until_disconnected(self: 'TelegramClient'):
|
||||
try:
|
||||
# Make a high-level request to notify that we want updates
|
||||
await self(functions.updates.GetStateRequest())
|
||||
result = await self.disconnected
|
||||
if self._updates_error is not None:
|
||||
raise self._updates_error
|
||||
return result
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
await self.disconnect()
|
||||
|
||||
async def set_receive_updates(self: 'TelegramClient', receive_updates):
|
||||
"""
|
||||
Change the value of `receive_updates`.
|
||||
|
||||
This is an `async` method, because in order for Telegram to start
|
||||
sending updates again, a request must be made.
|
||||
"""
|
||||
self._no_updates = not receive_updates
|
||||
if receive_updates:
|
||||
await self(functions.updates.GetStateRequest())
|
||||
|
||||
def run_until_disconnected(self: 'TelegramClient'):
|
||||
"""
|
||||
Runs the event loop until the library is disconnected.
|
||||
|
||||
It also notifies Telegram that we want to receive updates
|
||||
as described in https://core.telegram.org/api/updates.
|
||||
If an unexpected error occurs during update handling,
|
||||
the client will disconnect and said error will be raised.
|
||||
|
||||
Manual disconnections can be made by calling `disconnect()
|
||||
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`
|
||||
or sending a ``KeyboardInterrupt`` (e.g. by pressing ``Ctrl+C`` on
|
||||
the console window running the script).
|
||||
|
||||
If a disconnection error occurs (i.e. the library fails to reconnect
|
||||
automatically), said error will be raised through here, so you have a
|
||||
chance 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.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to handle ``KeyboardInterrupt`` in your code,
|
||||
simply run the event loop in your code too in any way, such as
|
||||
``loop.run_forever()`` or ``await client.disconnected`` (e.g.
|
||||
``loop.run_until_complete(client.disconnected)``).
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Blocks the current task here until a disconnection occurs.
|
||||
#
|
||||
# You will still receive updates, since this prevents the
|
||||
# script from exiting.
|
||||
await client.run_until_disconnected()
|
||||
"""
|
||||
if self.loop.is_running():
|
||||
return self._run_until_disconnected()
|
||||
try:
|
||||
return self.loop.run_until_complete(self._run_until_disconnected())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
# No loop.run_until_complete; it's already syncified
|
||||
self.disconnect()
|
||||
|
||||
def on(self: 'TelegramClient', event: EventBuilder):
|
||||
"""
|
||||
Decorator used to `add_event_handler` more conveniently.
|
||||
|
||||
|
||||
Arguments
|
||||
event (`_EventBuilder` | `type`):
|
||||
The event builder class or instance to be used,
|
||||
for instance ``events.NewMessage``.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, events
|
||||
client = TelegramClient(...)
|
||||
|
||||
# Here we use client.on
|
||||
@client.on(events.NewMessage)
|
||||
async def handler(event):
|
||||
...
|
||||
"""
|
||||
def decorator(f):
|
||||
self.add_event_handler(f, event)
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
def add_event_handler(
|
||||
self: 'TelegramClient',
|
||||
callback: Callback,
|
||||
event: EventBuilder = None):
|
||||
"""
|
||||
Registers a new event handler callback.
|
||||
|
||||
The callback will be called when the specified event occurs.
|
||||
|
||||
Arguments
|
||||
callback (`callable`):
|
||||
The callable function accepting one parameter to be used.
|
||||
|
||||
Note that if you have used `telethon.events.register` in
|
||||
the callback, ``event`` will be ignored, and instead the
|
||||
events you previously registered will be used.
|
||||
|
||||
event (`_EventBuilder` | `type`, optional):
|
||||
The event builder class or instance to be used,
|
||||
for instance ``events.NewMessage``.
|
||||
|
||||
If left unspecified, `telethon.events.raw.Raw` (the
|
||||
:tl:`Update` objects with no further processing) will
|
||||
be passed instead.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import TelegramClient, events
|
||||
client = TelegramClient(...)
|
||||
|
||||
async def handler(event):
|
||||
...
|
||||
|
||||
client.add_event_handler(handler, events.NewMessage)
|
||||
"""
|
||||
builders = events._get_handlers(callback)
|
||||
if builders is not None:
|
||||
for event in builders:
|
||||
self._event_builders.append((event, callback))
|
||||
return
|
||||
|
||||
if isinstance(event, type):
|
||||
event = event()
|
||||
elif not event:
|
||||
event = events.Raw()
|
||||
|
||||
self._event_builders.append((event, callback))
|
||||
|
||||
def remove_event_handler(
|
||||
self: 'TelegramClient',
|
||||
callback: Callback,
|
||||
event: EventBuilder = None) -> int:
|
||||
"""
|
||||
Inverse operation of `add_event_handler()`.
|
||||
|
||||
If no event is given, all events for this callback are removed.
|
||||
Returns how many callbacks were removed.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
@client.on(events.Raw)
|
||||
@client.on(events.NewMessage)
|
||||
async def handler(event):
|
||||
...
|
||||
|
||||
# Removes only the "Raw" handling
|
||||
# "handler" will still receive "events.NewMessage"
|
||||
client.remove_event_handler(handler, events.Raw)
|
||||
|
||||
# "handler" will stop receiving anything
|
||||
client.remove_event_handler(handler)
|
||||
"""
|
||||
found = 0
|
||||
if event and not isinstance(event, type):
|
||||
event = type(event)
|
||||
|
||||
i = len(self._event_builders)
|
||||
while i:
|
||||
i -= 1
|
||||
ev, cb = self._event_builders[i]
|
||||
if cb == callback and (not event or isinstance(ev, event)):
|
||||
del self._event_builders[i]
|
||||
found += 1
|
||||
|
||||
return found
|
||||
|
||||
def list_event_handlers(self: 'TelegramClient')\
|
||||
-> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]':
|
||||
"""
|
||||
Lists all registered event handlers.
|
||||
|
||||
Returns
|
||||
A list of pairs consisting of ``(callback, event)``.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
@client.on(events.NewMessage(pattern='hello'))
|
||||
async def on_greeting(event):
|
||||
'''Greets someone'''
|
||||
await event.reply('Hi')
|
||||
|
||||
for callback, event in client.list_event_handlers():
|
||||
print(id(callback), type(event))
|
||||
"""
|
||||
return [(callback, event) for event, callback in self._event_builders]
|
||||
|
||||
async def catch_up(self: 'TelegramClient'):
|
||||
"""
|
||||
"Catches up" on the missed updates while the client was offline.
|
||||
You should call this method after registering the event handlers
|
||||
so that the updates it loads can by processed by your script.
|
||||
|
||||
This can also be used to forcibly fetch new updates if there are any.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
await client.catch_up()
|
||||
"""
|
||||
await self._updates_queue.put(types.UpdatesTooLong())
|
||||
|
||||
# endregion
|
||||
|
||||
# region Private methods
|
||||
|
||||
async def _update_loop(self: 'TelegramClient'):
|
||||
# If the MessageBox is not empty, the account had to be logged-in to fill in its state.
|
||||
# This flag is used to propagate the "you got logged-out" error up (but getting logged-out
|
||||
# can only happen if it was once logged-in).
|
||||
was_once_logged_in = self._authorized is True or not self._message_box.is_empty()
|
||||
|
||||
self._updates_error = None
|
||||
try:
|
||||
if self._catch_up:
|
||||
# User wants to catch up as soon as the client is up and running,
|
||||
# so this is the best place to do it.
|
||||
await self.catch_up()
|
||||
|
||||
updates_to_dispatch = deque()
|
||||
|
||||
while self.is_connected():
|
||||
if updates_to_dispatch:
|
||||
if self._sequential_updates:
|
||||
await self._dispatch_update(updates_to_dispatch.popleft())
|
||||
else:
|
||||
while updates_to_dispatch:
|
||||
# TODO if _dispatch_update fails for whatever reason, it's not logged! this should be fixed
|
||||
task = self.loop.create_task(self._dispatch_update(updates_to_dispatch.popleft()))
|
||||
self._event_handler_tasks.add(task)
|
||||
task.add_done_callback(self._event_handler_tasks.discard)
|
||||
|
||||
continue
|
||||
|
||||
if len(self._mb_entity_cache) >= self._entity_cache_limit:
|
||||
self._log[__name__].info(
|
||||
'In-memory entity cache limit reached (%s/%s), flushing to session',
|
||||
len(self._mb_entity_cache),
|
||||
self._entity_cache_limit
|
||||
)
|
||||
self._save_states_and_entities()
|
||||
self._mb_entity_cache.retain(lambda id: id == self._mb_entity_cache.self_id or id in self._message_box.map)
|
||||
if len(self._mb_entity_cache) >= self._entity_cache_limit:
|
||||
warnings.warn('in-memory entities exceed entity_cache_limit after flushing; consider setting a larger limit')
|
||||
|
||||
self._log[__name__].info(
|
||||
'In-memory entity cache at %s/%s after flushing to session',
|
||||
len(self._mb_entity_cache),
|
||||
self._entity_cache_limit
|
||||
)
|
||||
|
||||
|
||||
get_diff = self._message_box.get_difference()
|
||||
if get_diff:
|
||||
self._log[__name__].debug('Getting difference for account updates')
|
||||
try:
|
||||
diff = await self(get_diff)
|
||||
except (errors.ServerError, errors.TimeoutError, ValueError) as e:
|
||||
# Telegram is having issues
|
||||
self._log[__name__].info('Cannot get difference since Telegram is having issues: %s', type(e).__name__)
|
||||
self._message_box.end_difference()
|
||||
continue
|
||||
except (errors.UnauthorizedError, errors.AuthKeyError) as e:
|
||||
# Not logged in or broken authorization key, can't get difference
|
||||
self._log[__name__].info('Cannot get difference since the account is not logged in: %s', type(e).__name__)
|
||||
self._message_box.end_difference()
|
||||
if was_once_logged_in:
|
||||
self._updates_error = e
|
||||
await self.disconnect()
|
||||
break
|
||||
continue
|
||||
except errors.TypeNotFoundError as e:
|
||||
# User is likely doing weird things with their account or session and Telegram gets confused as to what layer they use
|
||||
self._log[__name__].warning('Cannot get difference since the account is likely misusing the session: %s', e)
|
||||
self._message_box.end_difference()
|
||||
self._updates_error = e
|
||||
await self.disconnect()
|
||||
break
|
||||
except OSError as e:
|
||||
# Network is likely down, but it's unclear for how long.
|
||||
# If disconnect is called this task will be cancelled along with the sleep.
|
||||
# If disconnect is not called, getting difference should be retried after a few seconds.
|
||||
self._log[__name__].info('Cannot get difference since the network is down: %s: %s', type(e).__name__, e)
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
updates, users, chats = self._message_box.apply_difference(diff, self._mb_entity_cache)
|
||||
if updates:
|
||||
self._log[__name__].info('Got difference for account updates')
|
||||
|
||||
updates_to_dispatch.extend(self._preprocess_updates(updates, users, chats))
|
||||
continue
|
||||
|
||||
get_diff = self._message_box.get_channel_difference(self._mb_entity_cache)
|
||||
if get_diff:
|
||||
self._log[__name__].debug('Getting difference for channel %s updates', get_diff.channel.channel_id)
|
||||
try:
|
||||
diff = await self(get_diff)
|
||||
except (errors.UnauthorizedError, errors.AuthKeyError) as e:
|
||||
# Not logged in or broken authorization key, can't get difference
|
||||
self._log[__name__].warning(
|
||||
'Cannot get difference for channel %s since the account is not logged in: %s',
|
||||
get_diff.channel.channel_id, type(e).__name__
|
||||
)
|
||||
self._message_box.end_channel_difference(
|
||||
get_diff,
|
||||
PrematureEndReason.TEMPORARY_SERVER_ISSUES,
|
||||
self._mb_entity_cache
|
||||
)
|
||||
if was_once_logged_in:
|
||||
self._updates_error = e
|
||||
await self.disconnect()
|
||||
break
|
||||
continue
|
||||
except errors.TypeNotFoundError as e:
|
||||
self._log[__name__].warning(
|
||||
'Cannot get difference for channel %s since the account is likely misusing the session: %s',
|
||||
get_diff.channel.channel_id, e
|
||||
)
|
||||
self._message_box.end_channel_difference(
|
||||
get_diff,
|
||||
PrematureEndReason.TEMPORARY_SERVER_ISSUES,
|
||||
self._mb_entity_cache
|
||||
)
|
||||
self._updates_error = e
|
||||
await self.disconnect()
|
||||
break
|
||||
except (
|
||||
errors.PersistentTimestampOutdatedError,
|
||||
errors.PersistentTimestampInvalidError,
|
||||
errors.ServerError,
|
||||
errors.TimeoutError,
|
||||
ValueError
|
||||
) as e:
|
||||
# According to Telegram's docs:
|
||||
# "Channel internal replication issues, try again later (treat this like an RPC_CALL_FAIL)."
|
||||
# We can treat this as "empty difference" and not update the local pts.
|
||||
# Then this same call will be retried when another gap is detected or timeout expires.
|
||||
#
|
||||
# Another option would be to literally treat this like an RPC_CALL_FAIL and retry after a few
|
||||
# seconds, but if Telegram is having issues it's probably best to wait for it to send another
|
||||
# update (hinting it may be okay now) and retry then.
|
||||
#
|
||||
# This is a bit hacky because MessageBox doesn't really have a way to "not update" the pts.
|
||||
# Instead we manually extract the previously-known pts and use that.
|
||||
#
|
||||
# For PersistentTimestampInvalidError:
|
||||
# Somehow our pts is either too new or the server does not know about this.
|
||||
# We treat this as PersistentTimestampOutdatedError for now.
|
||||
# TODO investigate why/when this happens and if this is the proper solution
|
||||
self._log[__name__].warning(
|
||||
'Getting difference for channel updates %s caused %s;'
|
||||
' ending getting difference prematurely until server issues are resolved',
|
||||
get_diff.channel.channel_id, type(e).__name__
|
||||
)
|
||||
self._message_box.end_channel_difference(
|
||||
get_diff,
|
||||
PrematureEndReason.TEMPORARY_SERVER_ISSUES,
|
||||
self._mb_entity_cache
|
||||
)
|
||||
continue
|
||||
except (errors.ChannelPrivateError, errors.ChannelInvalidError):
|
||||
# Timeout triggered a get difference, but we have been banned in the channel since then.
|
||||
# Because we can no longer fetch updates from this channel, we should stop keeping track
|
||||
# of it entirely.
|
||||
self._log[__name__].info(
|
||||
'Account is now banned in %d so we can no longer fetch updates from it',
|
||||
get_diff.channel.channel_id
|
||||
)
|
||||
self._message_box.end_channel_difference(
|
||||
get_diff,
|
||||
PrematureEndReason.BANNED,
|
||||
self._mb_entity_cache
|
||||
)
|
||||
continue
|
||||
except OSError as e:
|
||||
self._log[__name__].info(
|
||||
'Cannot get difference for channel %d since the network is down: %s: %s',
|
||||
get_diff.channel.channel_id, type(e).__name__, e
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
|
||||
updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._mb_entity_cache)
|
||||
if updates:
|
||||
self._log[__name__].info('Got difference for channel %d updates', get_diff.channel.channel_id)
|
||||
|
||||
updates_to_dispatch.extend(self._preprocess_updates(updates, users, chats))
|
||||
continue
|
||||
|
||||
deadline = self._message_box.check_deadlines()
|
||||
deadline_delay = deadline - get_running_loop().time()
|
||||
if deadline_delay > 0:
|
||||
# Don't bother sleeping and timing out if the delay is already 0 (pollutes the logs).
|
||||
try:
|
||||
updates = await asyncio.wait_for(self._updates_queue.get(), deadline_delay)
|
||||
except asyncio.TimeoutError:
|
||||
self._log[__name__].debug('Timeout waiting for updates expired')
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
processed = []
|
||||
try:
|
||||
users, chats = self._message_box.process_updates(updates, self._mb_entity_cache, processed)
|
||||
except GapError:
|
||||
continue # get(_channel)_difference will start returning requests
|
||||
|
||||
updates_to_dispatch.extend(self._preprocess_updates(processed, users, chats))
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
self._log[__name__].exception(f'Fatal error handling updates (this is a bug in Telethon v{__version__}, please report it)')
|
||||
self._updates_error = e
|
||||
await self.disconnect()
|
||||
|
||||
def _preprocess_updates(self, updates, users, chats):
|
||||
self._mb_entity_cache.extend(users, chats)
|
||||
entities = {utils.get_peer_id(x): x
|
||||
for x in itertools.chain(users, chats)}
|
||||
for u in updates:
|
||||
u._entities = entities
|
||||
return updates
|
||||
|
||||
async def _keepalive_loop(self: 'TelegramClient'):
|
||||
# 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(
|
||||
self.disconnected, timeout=60
|
||||
)
|
||||
continue # We actually just want to act upon timeout
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except Exception:
|
||||
continue # Any disconnected exception should be ignored
|
||||
|
||||
# Check if we have any exported senders to clean-up periodically
|
||||
await self._clean_exported_senders()
|
||||
|
||||
# Don't bother sending pings until the low-level connection is
|
||||
# ready, otherwise a lot of pings will be batched to be sent upon
|
||||
# reconnect, when we really don't care about that.
|
||||
if not self._sender._transport_connected():
|
||||
continue
|
||||
|
||||
# We also don't really care about their result.
|
||||
# Just send them periodically.
|
||||
try:
|
||||
self._sender._keepalive_ping(rnd())
|
||||
except (ConnectionError, asyncio.CancelledError):
|
||||
return
|
||||
|
||||
# Entities and cached files are not saved when they are
|
||||
# inserted because this is a rather expensive operation
|
||||
# (default's sqlite3 takes ~0.1s to commit changes). Do
|
||||
# it every minute instead. No-op if there's nothing new.
|
||||
self._save_states_and_entities()
|
||||
|
||||
self.session.save()
|
||||
|
||||
async def _dispatch_update(self: 'TelegramClient', update):
|
||||
# TODO only used for AlbumHack, and MessageBox is not really designed for this
|
||||
others = None
|
||||
|
||||
if not self._mb_entity_cache.self_id:
|
||||
# Some updates require our own ID, so we must make sure
|
||||
# that the event builder has offline access to it. Calling
|
||||
# `get_me()` will cache it under `self._mb_entity_cache`.
|
||||
#
|
||||
# It will return `None` if we haven't logged in yet which is
|
||||
# fine, we will just retry next time anyway.
|
||||
try:
|
||||
await self.get_me(input_peer=True)
|
||||
except OSError:
|
||||
pass # might not have connection
|
||||
|
||||
built = EventBuilderDict(self, update, others)
|
||||
for conv_set in self._conversations.values():
|
||||
for conv in conv_set:
|
||||
ev = built[events.NewMessage]
|
||||
if ev:
|
||||
conv._on_new_message(ev)
|
||||
|
||||
ev = built[events.MessageEdited]
|
||||
if ev:
|
||||
conv._on_edit(ev)
|
||||
|
||||
ev = built[events.MessageRead]
|
||||
if ev:
|
||||
conv._on_read(ev)
|
||||
|
||||
if conv._custom:
|
||||
await conv._check_custom(built)
|
||||
|
||||
for builder, callback in self._event_builders:
|
||||
event = built[type(builder)]
|
||||
if not event:
|
||||
continue
|
||||
|
||||
if not builder.resolved:
|
||||
await builder.resolve(self)
|
||||
|
||||
filter = builder.filter(event)
|
||||
if inspect.isawaitable(filter):
|
||||
filter = await filter
|
||||
if not filter:
|
||||
continue
|
||||
|
||||
try:
|
||||
await callback(event)
|
||||
except errors.AlreadyInConversationError:
|
||||
name = getattr(callback, '__name__', repr(callback))
|
||||
self._log[__name__].debug(
|
||||
'Event handler "%s" already has an open conversation, '
|
||||
'ignoring new one', name)
|
||||
except events.StopPropagation:
|
||||
name = getattr(callback, '__name__', repr(callback))
|
||||
self._log[__name__].debug(
|
||||
'Event handler "%s" stopped chain of propagation '
|
||||
'for event %s.', name, type(event).__name__
|
||||
)
|
||||
break
|
||||
except Exception as e:
|
||||
if not isinstance(e, asyncio.CancelledError) or self.is_connected():
|
||||
name = getattr(callback, '__name__', repr(callback))
|
||||
self._log[__name__].exception('Unhandled exception on %s', name)
|
||||
|
||||
async def _dispatch_event(self: 'TelegramClient', event):
|
||||
"""
|
||||
Dispatches a single, out-of-order event. Used by `AlbumHack`.
|
||||
"""
|
||||
# We're duplicating a most logic from `_dispatch_update`, but all in
|
||||
# the name of speed; we don't want to make it worse for all updates
|
||||
# just because albums may need it.
|
||||
for builder, callback in self._event_builders:
|
||||
if isinstance(builder, events.Raw):
|
||||
continue
|
||||
if not isinstance(event, builder.Event):
|
||||
continue
|
||||
|
||||
if not builder.resolved:
|
||||
await builder.resolve(self)
|
||||
|
||||
filter = builder.filter(event)
|
||||
if inspect.isawaitable(filter):
|
||||
filter = await filter
|
||||
if not filter:
|
||||
continue
|
||||
|
||||
try:
|
||||
await callback(event)
|
||||
except errors.AlreadyInConversationError:
|
||||
name = getattr(callback, '__name__', repr(callback))
|
||||
self._log[__name__].debug(
|
||||
'Event handler "%s" already has an open conversation, '
|
||||
'ignoring new one', name)
|
||||
except events.StopPropagation:
|
||||
name = getattr(callback, '__name__', repr(callback))
|
||||
self._log[__name__].debug(
|
||||
'Event handler "%s" stopped chain of propagation '
|
||||
'for event %s.', name, type(event).__name__
|
||||
)
|
||||
break
|
||||
except Exception as e:
|
||||
if not isinstance(e, asyncio.CancelledError) or self.is_connected():
|
||||
name = getattr(callback, '__name__', repr(callback))
|
||||
self._log[__name__].exception('Unhandled exception on %s', name)
|
||||
|
||||
async def _handle_auto_reconnect(self: 'TelegramClient'):
|
||||
# TODO Catch-up
|
||||
# For now we make a high-level request to let Telegram
|
||||
# know we are still interested in receiving more updates.
|
||||
try:
|
||||
await self.get_me()
|
||||
except Exception as e:
|
||||
self._log[__name__].warning('Error executing high-level request '
|
||||
'after reconnect: %s: %s', type(e), e)
|
||||
|
||||
return
|
||||
try:
|
||||
self._log[__name__].info(
|
||||
'Asking for the current state after reconnect...')
|
||||
|
||||
# TODO consider:
|
||||
# If there aren't many updates while the client is disconnected
|
||||
# (I tried with up to 20), Telegram seems to send them without
|
||||
# asking for them (via updates.getDifference).
|
||||
#
|
||||
# On disconnection, the library should probably set a "need
|
||||
# difference" or "catching up" flag so that any new updates are
|
||||
# ignored, and then the library should call updates.getDifference
|
||||
# itself to fetch them.
|
||||
#
|
||||
# In any case (either there are too many updates and Telegram
|
||||
# didn't send them, or there isn't a lot and Telegram sent them
|
||||
# but we dropped them), we fetch the new difference to get all
|
||||
# missed updates. I feel like this would be the best solution.
|
||||
|
||||
# If a disconnection occurs, the old known state will be
|
||||
# the latest one we were aware of, so we can catch up since
|
||||
# the most recent state we were aware of.
|
||||
await self.catch_up()
|
||||
|
||||
self._log[__name__].info('Successfully fetched missed updates')
|
||||
except errors.RPCError as e:
|
||||
self._log[__name__].warning('Failed to get missed updates after '
|
||||
'reconnect: %r', e)
|
||||
except Exception:
|
||||
self._log[__name__].exception(
|
||||
'Unhandled exception while getting update difference after reconnect')
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
class EventBuilderDict:
|
||||
"""
|
||||
Helper "dictionary" to return events from types and cache them.
|
||||
"""
|
||||
def __init__(self, client: 'TelegramClient', update, others):
|
||||
self.client = client
|
||||
self.update = update
|
||||
self.others = others
|
||||
|
||||
def __getitem__(self, builder):
|
||||
try:
|
||||
return self.__dict__[builder]
|
||||
except KeyError:
|
||||
event = self.__dict__[builder] = builder.build(
|
||||
self.update, self.others, self.client._self_id)
|
||||
|
||||
if isinstance(event, EventCommon):
|
||||
event.original_update = self.update
|
||||
event._entities = self.update._entities
|
||||
event._set_client(self.client)
|
||||
elif event:
|
||||
event._client = self.client
|
||||
|
||||
return event
|
|
@ -1,789 +0,0 @@
|
|||
import hashlib
|
||||
import io
|
||||
import itertools
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import typing
|
||||
from io import BytesIO
|
||||
|
||||
from ..crypto import AES
|
||||
|
||||
from .. import utils, helpers, hints
|
||||
from ..tl import types, functions, custom
|
||||
|
||||
try:
|
||||
import PIL
|
||||
import PIL.Image
|
||||
except ImportError:
|
||||
PIL = None
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
||||
|
||||
class _CacheType:
|
||||
"""Like functools.partial but pretends to be the wrapped class."""
|
||||
def __init__(self, cls):
|
||||
self._cls = cls
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self._cls(*args, file_reference=b'', **kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._cls == other
|
||||
|
||||
|
||||
def _resize_photo_if_needed(
|
||||
file, is_image, width=2560, height=2560, background=(255, 255, 255)):
|
||||
|
||||
# https://github.com/telegramdesktop/tdesktop/blob/12905f0dcb9d513378e7db11989455a1b764ef75/Telegram/SourceFiles/boxes/photo_crop_box.cpp#L254
|
||||
if (not is_image
|
||||
or PIL is None
|
||||
or (isinstance(file, io.IOBase) and not file.seekable())):
|
||||
return file
|
||||
|
||||
if isinstance(file, bytes):
|
||||
file = io.BytesIO(file)
|
||||
|
||||
before = file.tell() if isinstance(file, io.IOBase) else None
|
||||
|
||||
try:
|
||||
# Don't use a `with` block for `image`, or `file` would be closed.
|
||||
# See https://github.com/LonamiWebs/Telethon/issues/1121 for more.
|
||||
image = PIL.Image.open(file)
|
||||
try:
|
||||
kwargs = {'exif': image.info['exif']}
|
||||
except KeyError:
|
||||
kwargs = {}
|
||||
|
||||
if image.width <= width and image.height <= height:
|
||||
return file
|
||||
|
||||
image.thumbnail((width, height), PIL.Image.ANTIALIAS)
|
||||
|
||||
alpha_index = image.mode.find('A')
|
||||
if alpha_index == -1:
|
||||
# If the image mode doesn't have alpha
|
||||
# channel then don't bother masking it away.
|
||||
result = image
|
||||
else:
|
||||
# We could save the resized image with the original format, but
|
||||
# JPEG often compresses better -> smaller size -> faster upload
|
||||
# We need to mask away the alpha channel ([3]), since otherwise
|
||||
# IOError is raised when trying to save alpha channels in JPEG.
|
||||
result = PIL.Image.new('RGB', image.size, background)
|
||||
result.paste(image, mask=image.split()[alpha_index])
|
||||
|
||||
buffer = io.BytesIO()
|
||||
result.save(buffer, 'JPEG', progressive=True, **kwargs)
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
|
||||
except IOError:
|
||||
return file
|
||||
finally:
|
||||
if before is not None:
|
||||
file.seek(before, io.SEEK_SET)
|
||||
|
||||
|
||||
class UploadMethods:
|
||||
|
||||
# region Public methods
|
||||
|
||||
async def send_file(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntityLike',
|
||||
file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]',
|
||||
*,
|
||||
caption: typing.Union[str, typing.Sequence[str]] = None,
|
||||
force_document: bool = False,
|
||||
file_size: int = None,
|
||||
clear_draft: bool = False,
|
||||
progress_callback: 'hints.ProgressCallback' = None,
|
||||
reply_to: 'hints.MessageIDLike' = None,
|
||||
attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,
|
||||
thumb: 'hints.FileLike' = None,
|
||||
allow_cache: bool = True,
|
||||
parse_mode: str = (),
|
||||
formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None,
|
||||
voice_note: bool = False,
|
||||
video_note: bool = False,
|
||||
buttons: typing.Optional['hints.MarkupLike'] = None,
|
||||
silent: bool = None,
|
||||
background: bool = None,
|
||||
supports_streaming: bool = False,
|
||||
schedule: 'hints.DateLike' = None,
|
||||
comment_to: 'typing.Union[int, types.Message]' = None,
|
||||
ttl: int = None,
|
||||
nosound_video: bool = None,
|
||||
**kwargs) -> 'types.Message':
|
||||
"""
|
||||
Sends message with the given file to the specified entity.
|
||||
|
||||
.. note::
|
||||
|
||||
If the ``hachoir3`` package (``hachoir`` module) is installed,
|
||||
it will be used to determine metadata from audio and video files.
|
||||
|
||||
If the ``pillow`` package is installed and you are sending a photo,
|
||||
it will be resized to fit within the maximum dimensions allowed
|
||||
by Telegram to avoid ``errors.PhotoInvalidDimensionsError``. This
|
||||
cannot be done if you are sending :tl:`InputFile`, however.
|
||||
|
||||
Arguments
|
||||
entity (`entity`):
|
||||
Who will receive the file.
|
||||
|
||||
file (`str` | `bytes` | `file` | `media`):
|
||||
The file to send, which can be one of:
|
||||
|
||||
* A local file path to an in-disk file. The file name
|
||||
will be the path's base name.
|
||||
|
||||
* A `bytes` byte array with the file's data to send
|
||||
(for example, by using ``text.encode('utf-8')``).
|
||||
A default file name will be used.
|
||||
|
||||
* A bytes `io.IOBase` stream over the file to send
|
||||
(for example, by using ``open(file, 'rb')``).
|
||||
Its ``.name`` property will be used for the file name,
|
||||
or a default if it doesn't have one.
|
||||
|
||||
* An external URL to a file over the internet. This will
|
||||
send the file as "external" media, and Telegram is the
|
||||
one that will fetch the media and send it.
|
||||
|
||||
* A Bot API-like ``file_id``. You can convert previously
|
||||
sent media to file IDs for later reusing with
|
||||
`telethon.utils.pack_bot_file_id`.
|
||||
|
||||
* A handle to an existing file (for example, if you sent a
|
||||
message with media before, you can use its ``message.media``
|
||||
as a file here).
|
||||
|
||||
* A handle to an uploaded file (from `upload_file`).
|
||||
|
||||
* A :tl:`InputMedia` instance. For example, if you want to
|
||||
send a dice use :tl:`InputMediaDice`, or if you want to
|
||||
send a contact use :tl:`InputMediaContact`.
|
||||
|
||||
To send an album, you should provide a list in this parameter.
|
||||
|
||||
If a list or similar is provided, the files in it will be
|
||||
sent as an album in the order in which they appear, sliced
|
||||
in chunks of 10 if more than 10 are given.
|
||||
|
||||
caption (`str`, optional):
|
||||
Optional caption for the sent media message. When sending an
|
||||
album, the caption may be a list of strings, which will be
|
||||
assigned to the files pairwise.
|
||||
|
||||
force_document (`bool`, optional):
|
||||
If left to `False` and the file is a path that ends with
|
||||
the extension of an image file or a video file, it will be
|
||||
sent as such. Otherwise always as a document.
|
||||
|
||||
file_size (`int`, optional):
|
||||
The size of the file to be uploaded if it needs to be uploaded,
|
||||
which will be determined automatically if not specified.
|
||||
|
||||
If the file size can't be determined beforehand, the entire
|
||||
file will be read in-memory to find out how large it is.
|
||||
|
||||
clear_draft (`bool`, optional):
|
||||
Whether the existing draft should be cleared or not.
|
||||
|
||||
progress_callback (`callable`, optional):
|
||||
A callback function accepting two parameters:
|
||||
``(sent bytes, total)``.
|
||||
|
||||
reply_to (`int` | `Message <telethon.tl.custom.message.Message>`):
|
||||
Same as `reply_to` from `send_message`.
|
||||
|
||||
attributes (`list`, optional):
|
||||
Optional attributes that override the inferred ones, like
|
||||
:tl:`DocumentAttributeFilename` and so on.
|
||||
|
||||
thumb (`str` | `bytes` | `file`, optional):
|
||||
Optional JPEG thumbnail (for documents). **Telegram will
|
||||
ignore this parameter** unless you pass a ``.jpg`` file!
|
||||
|
||||
The file must also be small in dimensions and in disk size.
|
||||
Successful thumbnails were files below 20kB and 320x320px.
|
||||
Width/height and dimensions/size ratios may be important.
|
||||
For Telegram to accept a thumbnail, you must provide the
|
||||
dimensions of the underlying media through ``attributes=``
|
||||
with :tl:`DocumentAttributesVideo` or by installing the
|
||||
optional ``hachoir`` dependency.
|
||||
|
||||
|
||||
allow_cache (`bool`, optional):
|
||||
This parameter currently does nothing, but is kept for
|
||||
backward-compatibility (and it may get its use back in
|
||||
the future).
|
||||
|
||||
parse_mode (`object`, optional):
|
||||
See the `TelegramClient.parse_mode
|
||||
<telethon.client.messageparse.MessageParseMethods.parse_mode>`
|
||||
property for allowed values. Markdown parsing will be used by
|
||||
default.
|
||||
|
||||
formatting_entities (`list`, optional):
|
||||
A list of message formatting entities. When provided, the ``parse_mode`` is ignored.
|
||||
|
||||
voice_note (`bool`, optional):
|
||||
If `True` the audio will be sent as a voice note.
|
||||
|
||||
video_note (`bool`, optional):
|
||||
If `True` the video will be sent as a video note,
|
||||
also known as a round video message.
|
||||
|
||||
buttons (`list`, `custom.Button <telethon.tl.custom.button.Button>`, :tl:`KeyboardButton`):
|
||||
The matrix (list of lists), row list or button to be shown
|
||||
after sending the message. This parameter will only work if
|
||||
you have signed in as a bot. You can also pass your own
|
||||
:tl:`ReplyMarkup` here.
|
||||
|
||||
silent (`bool`, optional):
|
||||
Whether the message should notify people with sound or not.
|
||||
Defaults to `False` (send with a notification sound unless
|
||||
the person has the chat muted). Set it to `True` to alter
|
||||
this behaviour.
|
||||
|
||||
background (`bool`, optional):
|
||||
Whether the message should be send in background.
|
||||
|
||||
supports_streaming (`bool`, optional):
|
||||
Whether the sent video supports streaming or not. Note that
|
||||
Telegram only recognizes as streamable some formats like MP4,
|
||||
and others like AVI or MKV will not work. You should convert
|
||||
these to MP4 before sending if you want them to be streamable.
|
||||
Unsupported formats will result in ``VideoContentTypeError``.
|
||||
|
||||
schedule (`hints.DateLike`, optional):
|
||||
If set, the file won't send immediately, and instead
|
||||
it will be scheduled to be automatically sent at a later
|
||||
time.
|
||||
|
||||
comment_to (`int` | `Message <telethon.tl.custom.message.Message>`, optional):
|
||||
Similar to ``reply_to``, but replies in the linked group of a
|
||||
broadcast channel instead (effectively leaving a "comment to"
|
||||
the specified message).
|
||||
|
||||
This parameter takes precedence over ``reply_to``. If there is
|
||||
no linked chat, `telethon.errors.sgIdInvalidError` is raised.
|
||||
|
||||
ttl (`int`. optional):
|
||||
The Time-To-Live of the file (also known as "self-destruct timer"
|
||||
or "self-destructing media"). If set, files can only be viewed for
|
||||
a short period of time before they disappear from the message
|
||||
history automatically.
|
||||
|
||||
The value must be at least 1 second, and at most 60 seconds,
|
||||
otherwise Telegram will ignore this parameter.
|
||||
|
||||
Not all types of media can be used with this parameter, such
|
||||
as text documents, which will fail with ``TtlMediaInvalidError``.
|
||||
|
||||
nosound_video (`bool`, optional):
|
||||
Only applicable when sending a video file without an audio
|
||||
track. If set to ``True``, the video will be displayed in
|
||||
Telegram as a video. If set to ``False``, Telegram will attempt
|
||||
to display the video as an animated gif. (It may still display
|
||||
as a video due to other factors.) The value is ignored if set
|
||||
on non-video files. This is set to ``True`` for albums, as gifs
|
||||
cannot be sent in albums.
|
||||
|
||||
Returns
|
||||
The `Message <telethon.tl.custom.message.Message>` (or messages)
|
||||
containing the sent file, or messages if a list of them was passed.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Normal files like photos
|
||||
await client.send_file(chat, '/my/photos/me.jpg', caption="It's me!")
|
||||
# or
|
||||
await client.send_message(chat, "It's me!", file='/my/photos/me.jpg')
|
||||
|
||||
# Voice notes or round videos
|
||||
await client.send_file(chat, '/my/songs/song.mp3', voice_note=True)
|
||||
await client.send_file(chat, '/my/videos/video.mp4', video_note=True)
|
||||
|
||||
# Custom thumbnails
|
||||
await client.send_file(chat, '/my/documents/doc.txt', thumb='photo.jpg')
|
||||
|
||||
# Only documents
|
||||
await client.send_file(chat, '/my/photos/photo.png', force_document=True)
|
||||
|
||||
# Albums
|
||||
await client.send_file(chat, [
|
||||
'/my/photos/holiday1.jpg',
|
||||
'/my/photos/holiday2.jpg',
|
||||
'/my/drawings/portrait.png'
|
||||
])
|
||||
|
||||
# Printing upload progress
|
||||
def callback(current, total):
|
||||
print('Uploaded', current, 'out of', total,
|
||||
'bytes: {:.2%}'.format(current / total))
|
||||
|
||||
await client.send_file(chat, file, progress_callback=callback)
|
||||
|
||||
# Dices, including dart and other future emoji
|
||||
from telethon.tl import types
|
||||
await client.send_file(chat, types.InputMediaDice(''))
|
||||
await client.send_file(chat, types.InputMediaDice('🎯'))
|
||||
|
||||
# Contacts
|
||||
await client.send_file(chat, types.InputMediaContact(
|
||||
phone_number='+34 123 456 789',
|
||||
first_name='Example',
|
||||
last_name='',
|
||||
vcard=''
|
||||
))
|
||||
"""
|
||||
# TODO Properly implement allow_cache to reuse the sha256 of the file
|
||||
# i.e. `None` was used
|
||||
if not file:
|
||||
raise TypeError('Cannot use {!r} as file'.format(file))
|
||||
|
||||
if not caption:
|
||||
caption = ''
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
if comment_to is not None:
|
||||
entity, reply_to = await self._get_comment_data(entity, comment_to)
|
||||
else:
|
||||
reply_to = utils.get_message_id(reply_to)
|
||||
|
||||
# First check if the user passed an iterable, in which case
|
||||
# we may want to send grouped.
|
||||
if utils.is_list_like(file):
|
||||
sent_count = 0
|
||||
used_callback = None if not progress_callback else (
|
||||
lambda s, t: progress_callback(sent_count + s, len(file))
|
||||
)
|
||||
|
||||
if utils.is_list_like(caption):
|
||||
captions = caption
|
||||
else:
|
||||
captions = [caption]
|
||||
|
||||
result = []
|
||||
while file:
|
||||
result += await self._send_album(
|
||||
entity, file[:10], caption=captions[:10],
|
||||
progress_callback=used_callback, reply_to=reply_to,
|
||||
parse_mode=parse_mode, silent=silent, schedule=schedule,
|
||||
supports_streaming=supports_streaming, clear_draft=clear_draft,
|
||||
force_document=force_document, background=background,
|
||||
)
|
||||
file = file[10:]
|
||||
captions = captions[10:]
|
||||
sent_count += 10
|
||||
|
||||
return result
|
||||
|
||||
if formatting_entities is not None:
|
||||
msg_entities = formatting_entities
|
||||
else:
|
||||
caption, msg_entities =\
|
||||
await self._parse_message_text(caption, parse_mode)
|
||||
|
||||
file_handle, media, image = await self._file_to_media(
|
||||
file, force_document=force_document,
|
||||
file_size=file_size,
|
||||
progress_callback=progress_callback,
|
||||
attributes=attributes, allow_cache=allow_cache, thumb=thumb,
|
||||
voice_note=voice_note, video_note=video_note,
|
||||
supports_streaming=supports_streaming, ttl=ttl,
|
||||
nosound_video=nosound_video,
|
||||
)
|
||||
|
||||
# e.g. invalid cast from :tl:`MessageMediaWebPage`
|
||||
if not media:
|
||||
raise TypeError('Cannot use {!r} as file'.format(file))
|
||||
|
||||
markup = self.build_reply_markup(buttons)
|
||||
request = functions.messages.SendMediaRequest(
|
||||
entity, media, reply_to_msg_id=reply_to, message=caption,
|
||||
entities=msg_entities, reply_markup=markup, silent=silent,
|
||||
schedule_date=schedule, clear_draft=clear_draft,
|
||||
background=background
|
||||
)
|
||||
return self._get_response_message(request, await self(request), entity)
|
||||
|
||||
async def _send_album(self: 'TelegramClient', entity, files, caption='',
|
||||
progress_callback=None, reply_to=None,
|
||||
parse_mode=(), silent=None, schedule=None,
|
||||
supports_streaming=None, clear_draft=None,
|
||||
force_document=False, background=None, ttl=None):
|
||||
"""Specialized version of .send_file for albums"""
|
||||
# We don't care if the user wants to avoid cache, we will use it
|
||||
# anyway. Why? The cached version will be exactly the same thing
|
||||
# we need to produce right now to send albums (uploadMedia), and
|
||||
# cache only makes a difference for documents where the user may
|
||||
# want the attributes used on them to change.
|
||||
#
|
||||
# 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)
|
||||
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))
|
||||
|
||||
reply_to = utils.get_message_id(reply_to)
|
||||
|
||||
used_callback = None if not progress_callback else (
|
||||
# use an integer when sent matches total, to easily determine a file has been fully sent
|
||||
lambda s, t: progress_callback(sent_count + 1 if s == t else sent_count + s / t, len(files))
|
||||
)
|
||||
|
||||
# Need to upload the media first, but only if they're not cached yet
|
||||
media = []
|
||||
for sent_count, file in enumerate(files):
|
||||
# Albums want :tl:`InputMedia` which, in theory, includes
|
||||
# :tl:`InputMediaUploadedPhoto`. However, using that will
|
||||
# make it `raise MediaInvalidError`, so we need to upload
|
||||
# it as media and then convert that to :tl:`InputMediaPhoto`.
|
||||
fh, fm, _ = await self._file_to_media(
|
||||
file, supports_streaming=supports_streaming,
|
||||
force_document=force_document, ttl=ttl,
|
||||
progress_callback=used_callback, nosound_video=True)
|
||||
if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)):
|
||||
r = await self(functions.messages.UploadMediaRequest(
|
||||
entity, media=fm
|
||||
))
|
||||
|
||||
fm = utils.get_input_media(r.photo)
|
||||
elif isinstance(fm, types.InputMediaUploadedDocument):
|
||||
r = await self(functions.messages.UploadMediaRequest(
|
||||
entity, media=fm
|
||||
))
|
||||
|
||||
fm = utils.get_input_media(
|
||||
r.document, supports_streaming=supports_streaming)
|
||||
|
||||
if captions:
|
||||
caption, msg_entities = captions.pop()
|
||||
else:
|
||||
caption, msg_entities = '', None
|
||||
media.append(types.InputSingleMedia(
|
||||
fm,
|
||||
message=caption,
|
||||
entities=msg_entities
|
||||
# random_id is autogenerated
|
||||
))
|
||||
|
||||
# Now we can construct the multi-media request
|
||||
request = functions.messages.SendMultiMediaRequest(
|
||||
entity, reply_to_msg_id=reply_to, multi_media=media,
|
||||
silent=silent, schedule_date=schedule, clear_draft=clear_draft,
|
||||
background=background
|
||||
)
|
||||
result = await self(request)
|
||||
|
||||
random_ids = [m.random_id for m in media]
|
||||
return self._get_response_message(random_ids, result, entity)
|
||||
|
||||
async def upload_file(
|
||||
self: 'TelegramClient',
|
||||
file: 'hints.FileLike',
|
||||
*,
|
||||
part_size_kb: float = None,
|
||||
file_size: int = None,
|
||||
file_name: str = None,
|
||||
use_cache: type = None,
|
||||
key: bytes = None,
|
||||
iv: bytes = None,
|
||||
progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile':
|
||||
"""
|
||||
Uploads a file to Telegram's servers, without sending it.
|
||||
|
||||
.. note::
|
||||
|
||||
Generally, you want to use `send_file` instead.
|
||||
|
||||
This method returns a handle (an instance of :tl:`InputFile` or
|
||||
:tl:`InputFileBig`, as required) which can be later used before
|
||||
it expires (they are usable during less than a day).
|
||||
|
||||
Uploading a file will simply return a "handle" to the file stored
|
||||
remotely in the Telegram servers, which can be later used on. This
|
||||
will **not** upload the file to your own chat or any chat at all.
|
||||
|
||||
Arguments
|
||||
file (`str` | `bytes` | `file`):
|
||||
The path of the file, byte array, or stream that will be sent.
|
||||
Note that if a byte array or a stream is given, a filename
|
||||
or its type won't be inferred, and it will be sent as an
|
||||
"unnamed application/octet-stream".
|
||||
|
||||
part_size_kb (`int`, optional):
|
||||
Chunk size when uploading files. The larger, the less
|
||||
requests will be made (up to 512KB maximum).
|
||||
|
||||
file_size (`int`, optional):
|
||||
The size of the file to be uploaded, which will be determined
|
||||
automatically if not specified.
|
||||
|
||||
If the file size can't be determined beforehand, the entire
|
||||
file will be read in-memory to find out how large it is.
|
||||
|
||||
file_name (`str`, optional):
|
||||
The file name which will be used on the resulting InputFile.
|
||||
If not specified, the name will be taken from the ``file``
|
||||
and if this is not a `str`, it will be ``"unnamed"``.
|
||||
|
||||
use_cache (`type`, optional):
|
||||
This parameter currently does nothing, but is kept for
|
||||
backward-compatibility (and it may get its use back in
|
||||
the future).
|
||||
|
||||
key ('bytes', optional):
|
||||
In case of an encrypted upload (secret chats) a key is supplied
|
||||
|
||||
iv ('bytes', optional):
|
||||
In case of an encrypted upload (secret chats) an iv is supplied
|
||||
|
||||
progress_callback (`callable`, optional):
|
||||
A callback function accepting two parameters:
|
||||
``(sent bytes, total)``.
|
||||
|
||||
When sending an album, the callback will receive a number
|
||||
between 0 and the amount of files as the "sent" parameter,
|
||||
and the amount of files as the "total". Note that the first
|
||||
parameter will be a floating point number to indicate progress
|
||||
within a file (e.g. ``2.5`` means it has sent 50% of the third
|
||||
file, because it's between 2 and 3).
|
||||
|
||||
Returns
|
||||
:tl:`InputFileBig` if the file size is larger than 10MB,
|
||||
`InputSizedFile <telethon.tl.custom.inputsizedfile.InputSizedFile>`
|
||||
(subclass of :tl:`InputFile`) otherwise.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Photos as photo and document
|
||||
file = await client.upload_file('photo.jpg')
|
||||
await client.send_file(chat, file) # sends as photo
|
||||
await client.send_file(chat, file, force_document=True) # sends as document
|
||||
|
||||
file.name = 'not a photo.jpg'
|
||||
await client.send_file(chat, file, force_document=True) # document, new name
|
||||
|
||||
# As song or as voice note
|
||||
file = await client.upload_file('song.ogg')
|
||||
await client.send_file(chat, file) # sends as song
|
||||
await client.send_file(chat, file, voice_note=True) # sends as voice note
|
||||
"""
|
||||
if isinstance(file, (types.InputFile, types.InputFileBig)):
|
||||
return file # Already uploaded
|
||||
|
||||
pos = 0
|
||||
async with helpers._FileStream(file, file_size=file_size) as stream:
|
||||
# Opening the stream will determine the correct file size
|
||||
file_size = stream.file_size
|
||||
|
||||
if not part_size_kb:
|
||||
part_size_kb = utils.get_appropriated_part_size(file_size)
|
||||
|
||||
if part_size_kb > 512:
|
||||
raise ValueError('The part size must be less or equal to 512KB')
|
||||
|
||||
part_size = int(part_size_kb * 1024)
|
||||
if part_size % 1024 != 0:
|
||||
raise ValueError(
|
||||
'The part size must be evenly divisible by 1024')
|
||||
|
||||
# Set a default file name if None was specified
|
||||
file_id = helpers.generate_random_long()
|
||||
if not file_name:
|
||||
file_name = stream.name or str(file_id)
|
||||
|
||||
# If the file name lacks extension, add it if possible.
|
||||
# Else Telegram complains with `PHOTO_EXT_INVALID_ERROR`
|
||||
# even if the uploaded image is indeed a photo.
|
||||
if not os.path.splitext(file_name)[-1]:
|
||||
file_name += utils._get_extension(stream)
|
||||
|
||||
# Determine whether the file is too big (over 10MB) or not
|
||||
# Telegram does make a distinction between smaller or larger files
|
||||
is_big = file_size > 10 * 1024 * 1024
|
||||
hash_md5 = hashlib.md5()
|
||||
|
||||
part_count = (file_size + part_size - 1) // part_size
|
||||
self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d',
|
||||
file_size, part_count, part_size)
|
||||
|
||||
pos = 0
|
||||
for part_index in range(part_count):
|
||||
# Read the file by in chunks of size part_size
|
||||
part = await helpers._maybe_await(stream.read(part_size))
|
||||
|
||||
if not isinstance(part, bytes):
|
||||
raise TypeError(
|
||||
'file descriptor returned {}, not bytes (you must '
|
||||
'open the file in bytes mode)'.format(type(part)))
|
||||
|
||||
# `file_size` could be wrong in which case `part` may not be
|
||||
# `part_size` before reaching the end.
|
||||
if len(part) != part_size and part_index < part_count - 1:
|
||||
raise ValueError(
|
||||
'read less than {} before reaching the end; either '
|
||||
'`file_size` or `read` are wrong'.format(part_size))
|
||||
|
||||
pos += len(part)
|
||||
|
||||
# Encryption part if needed
|
||||
if key and iv:
|
||||
part = AES.encrypt_ige(part, key, iv)
|
||||
|
||||
if not is_big:
|
||||
# Bit odd that MD5 is only needed for small files and not
|
||||
# big ones with more chance for corruption, but that's
|
||||
# what Telegram wants.
|
||||
hash_md5.update(part)
|
||||
|
||||
# The SavePartRequest is different depending on whether
|
||||
# the file is too large or not (over or less than 10MB)
|
||||
if is_big:
|
||||
request = functions.upload.SaveBigFilePartRequest(
|
||||
file_id, part_index, part_count, part)
|
||||
else:
|
||||
request = functions.upload.SaveFilePartRequest(
|
||||
file_id, part_index, part)
|
||||
|
||||
result = await self(request)
|
||||
if result:
|
||||
self._log[__name__].debug('Uploaded %d/%d',
|
||||
part_index + 1, part_count)
|
||||
if progress_callback:
|
||||
await helpers._maybe_await(progress_callback(pos, file_size))
|
||||
else:
|
||||
raise RuntimeError(
|
||||
'Failed to upload file part {}.'.format(part_index))
|
||||
|
||||
if is_big:
|
||||
return types.InputFileBig(file_id, part_count, file_name)
|
||||
else:
|
||||
return custom.InputSizedFile(
|
||||
file_id, part_count, file_name, md5=hash_md5, size=file_size
|
||||
)
|
||||
|
||||
# endregion
|
||||
|
||||
async def _file_to_media(
|
||||
self, file, force_document=False, file_size=None,
|
||||
progress_callback=None, attributes=None, thumb=None,
|
||||
allow_cache=True, voice_note=False, video_note=False,
|
||||
supports_streaming=False, mime_type=None, as_image=None,
|
||||
ttl=None, nosound_video=None):
|
||||
if not file:
|
||||
return None, None, None
|
||||
|
||||
if isinstance(file, pathlib.Path):
|
||||
file = str(file.absolute())
|
||||
|
||||
is_image = utils.is_image(file)
|
||||
if as_image is None:
|
||||
as_image = is_image and not force_document
|
||||
|
||||
# `aiofiles` do not base `io.IOBase` but do have `read`, so we
|
||||
# just check for the read attribute to see if it's file-like.
|
||||
if not isinstance(file, (str, bytes, types.InputFile, types.InputFileBig))\
|
||||
and not hasattr(file, 'read'):
|
||||
# The user may pass a Message containing media (or the media,
|
||||
# or anything similar) that should be treated as a file. Try
|
||||
# getting the input media for whatever they passed and send it.
|
||||
#
|
||||
# We pass all attributes since these will be used if the user
|
||||
# passed :tl:`InputFile`, and all information may be relevant.
|
||||
try:
|
||||
return (None, utils.get_input_media(
|
||||
file,
|
||||
is_photo=as_image,
|
||||
attributes=attributes,
|
||||
force_document=force_document,
|
||||
voice_note=voice_note,
|
||||
video_note=video_note,
|
||||
supports_streaming=supports_streaming,
|
||||
ttl=ttl
|
||||
), as_image)
|
||||
except TypeError:
|
||||
# Can't turn whatever was given into media
|
||||
return None, None, as_image
|
||||
|
||||
media = None
|
||||
file_handle = None
|
||||
|
||||
if isinstance(file, (types.InputFile, types.InputFileBig)):
|
||||
file_handle = file
|
||||
elif not isinstance(file, str) or os.path.isfile(file):
|
||||
file_handle = await self.upload_file(
|
||||
_resize_photo_if_needed(file, as_image),
|
||||
file_size=file_size,
|
||||
progress_callback=progress_callback
|
||||
)
|
||||
elif re.match('https?://', file):
|
||||
if as_image:
|
||||
media = types.InputMediaPhotoExternal(file, ttl_seconds=ttl)
|
||||
else:
|
||||
media = types.InputMediaDocumentExternal(file, ttl_seconds=ttl)
|
||||
else:
|
||||
bot_file = utils.resolve_bot_file_id(file)
|
||||
if bot_file:
|
||||
media = utils.get_input_media(bot_file, ttl=ttl)
|
||||
|
||||
if media:
|
||||
pass # Already have media, don't check the rest
|
||||
elif not file_handle:
|
||||
raise ValueError(
|
||||
'Failed to convert {} to media. Not an existing file, '
|
||||
'an HTTP URL or a valid bot-API-like file ID'.format(file)
|
||||
)
|
||||
elif as_image:
|
||||
media = types.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl)
|
||||
else:
|
||||
attributes, mime_type = utils.get_attributes(
|
||||
file,
|
||||
mime_type=mime_type,
|
||||
attributes=attributes,
|
||||
force_document=force_document and not is_image,
|
||||
voice_note=voice_note,
|
||||
video_note=video_note,
|
||||
supports_streaming=supports_streaming,
|
||||
thumb=thumb
|
||||
)
|
||||
|
||||
if not thumb:
|
||||
thumb = None
|
||||
else:
|
||||
if isinstance(thumb, pathlib.Path):
|
||||
thumb = str(thumb.absolute())
|
||||
thumb = await self.upload_file(thumb, file_size=file_size)
|
||||
|
||||
# setting `nosound_video` to `True` doesn't affect videos with sound
|
||||
# instead it prevents sending silent videos as GIFs
|
||||
nosound_video = nosound_video if mime_type.split("/")[0] == 'video' else None
|
||||
|
||||
media = types.InputMediaUploadedDocument(
|
||||
file=file_handle,
|
||||
mime_type=mime_type,
|
||||
attributes=attributes,
|
||||
thumb=thumb,
|
||||
force_file=force_document and not is_image,
|
||||
ttl_seconds=ttl,
|
||||
nosound_video=nosound_video
|
||||
)
|
||||
return file_handle, media, as_image
|
||||
|
||||
# endregion
|
|
@ -1,612 +0,0 @@
|
|||
import asyncio
|
||||
import datetime
|
||||
import itertools
|
||||
import time
|
||||
import typing
|
||||
|
||||
from .. import errors, helpers, utils, hints
|
||||
from ..errors import MultiError, RPCError
|
||||
from ..helpers import retry_range
|
||||
from ..tl import TLRequest, types, functions
|
||||
|
||||
_NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!')
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .telegramclient import TelegramClient
|
||||
|
||||
|
||||
def _fmt_flood(delay, request, *, early=False, td=datetime.timedelta):
|
||||
return (
|
||||
'Sleeping%s for %ds (%s) on %s flood wait',
|
||||
' early' if early else '',
|
||||
delay,
|
||||
td(seconds=delay),
|
||||
request.__class__.__name__
|
||||
)
|
||||
|
||||
|
||||
class UserMethods:
|
||||
async def __call__(self: 'TelegramClient', request, ordered=False, flood_sleep_threshold=None):
|
||||
return await self._call(self._sender, request, ordered=ordered)
|
||||
|
||||
async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None):
|
||||
if self._loop is not None and self._loop != helpers.get_running_loop():
|
||||
raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)')
|
||||
# if the loop is None it will fail with a connection error later on
|
||||
|
||||
if flood_sleep_threshold is None:
|
||||
flood_sleep_threshold = self.flood_sleep_threshold
|
||||
requests = (request if utils.is_list_like(request) else (request,))
|
||||
for r in requests:
|
||||
if not isinstance(r, TLRequest):
|
||||
raise _NOT_A_REQUEST()
|
||||
await r.resolve(self, utils)
|
||||
|
||||
# Avoid making the request if it's already in a flood wait
|
||||
if r.CONSTRUCTOR_ID in self._flood_waited_requests:
|
||||
due = self._flood_waited_requests[r.CONSTRUCTOR_ID]
|
||||
diff = round(due - time.time())
|
||||
if diff <= 3: # Flood waits below 3 seconds are "ignored"
|
||||
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
|
||||
elif diff <= flood_sleep_threshold:
|
||||
self._log[__name__].info(*_fmt_flood(diff, r, early=True))
|
||||
await asyncio.sleep(diff)
|
||||
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
|
||||
else:
|
||||
raise errors.FloodWaitError(request=r, capture=diff)
|
||||
|
||||
if self._no_updates:
|
||||
r = functions.InvokeWithoutUpdatesRequest(r)
|
||||
|
||||
request_index = 0
|
||||
last_error = None
|
||||
self._last_request = time.time()
|
||||
|
||||
for attempt in retry_range(self._request_retries):
|
||||
try:
|
||||
future = sender.send(request, ordered=ordered)
|
||||
if isinstance(future, list):
|
||||
results = []
|
||||
exceptions = []
|
||||
for f in future:
|
||||
try:
|
||||
result = await f
|
||||
except RPCError as e:
|
||||
exceptions.append(e)
|
||||
results.append(None)
|
||||
continue
|
||||
self.session.process_entities(result)
|
||||
exceptions.append(None)
|
||||
results.append(result)
|
||||
request_index += 1
|
||||
if any(x is not None for x in exceptions):
|
||||
raise MultiError(exceptions, results, requests)
|
||||
else:
|
||||
return results
|
||||
else:
|
||||
result = await future
|
||||
self.session.process_entities(result)
|
||||
return result
|
||||
except (errors.ServerError, errors.RpcCallFailError,
|
||||
errors.RpcMcgetFailError, errors.InterdcCallErrorError,
|
||||
errors.InterdcCallRichErrorError) as e:
|
||||
last_error = e
|
||||
self._log[__name__].warning(
|
||||
'Telegram is having internal issues %s: %s',
|
||||
e.__class__.__name__, e)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
except (errors.FloodWaitError, errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e:
|
||||
last_error = e
|
||||
if utils.is_list_like(request):
|
||||
request = request[request_index]
|
||||
|
||||
# SLOW_MODE_WAIT is chat-specific, not request-specific
|
||||
if not isinstance(e, errors.SlowModeWaitError):
|
||||
self._flood_waited_requests\
|
||||
[request.CONSTRUCTOR_ID] = time.time() + e.seconds
|
||||
|
||||
# In test servers, FLOOD_WAIT_0 has been observed, and sleeping for
|
||||
# such a short amount will cause retries very fast leading to issues.
|
||||
if e.seconds == 0:
|
||||
e.seconds = 1
|
||||
|
||||
if e.seconds <= self.flood_sleep_threshold:
|
||||
self._log[__name__].info(*_fmt_flood(e.seconds, request))
|
||||
await asyncio.sleep(e.seconds)
|
||||
else:
|
||||
raise
|
||||
except (errors.PhoneMigrateError, errors.NetworkMigrateError,
|
||||
errors.UserMigrateError) as e:
|
||||
last_error = e
|
||||
self._log[__name__].info('Phone migrated to %d', e.new_dc)
|
||||
should_raise = isinstance(e, (
|
||||
errors.PhoneMigrateError, errors.NetworkMigrateError
|
||||
))
|
||||
if should_raise and await self.is_user_authorized():
|
||||
raise
|
||||
await self._switch_dc(e.new_dc)
|
||||
|
||||
if self._raise_last_call_error and last_error is not None:
|
||||
raise last_error
|
||||
raise ValueError('Request was unsuccessful {} time(s)'
|
||||
.format(attempt))
|
||||
|
||||
# region Public methods
|
||||
|
||||
async def get_me(self: 'TelegramClient', input_peer: bool = False) \
|
||||
-> 'typing.Union[types.User, types.InputPeerUser]':
|
||||
"""
|
||||
Gets "me", the current :tl:`User` who is logged in.
|
||||
|
||||
If the user has not logged in yet, this method returns `None`.
|
||||
|
||||
Arguments
|
||||
input_peer (`bool`, optional):
|
||||
Whether to return the :tl:`InputPeerUser` version or the normal
|
||||
:tl:`User`. This can be useful if you just need to know the ID
|
||||
of yourself.
|
||||
|
||||
Returns
|
||||
Your own :tl:`User`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
me = await client.get_me()
|
||||
print(me.username)
|
||||
"""
|
||||
if input_peer and self._mb_entity_cache.self_id:
|
||||
return self._mb_entity_cache.get(self._mb_entity_cache.self_id)._as_input_peer()
|
||||
|
||||
try:
|
||||
me = (await self(
|
||||
functions.users.GetUsersRequest([types.InputUserSelf()])))[0]
|
||||
|
||||
if not self._mb_entity_cache.self_id:
|
||||
self._mb_entity_cache.set_self_user(me.id, me.bot, me.access_hash)
|
||||
|
||||
return utils.get_input_peer(me, allow_self=False) if input_peer else me
|
||||
except errors.UnauthorizedError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def _self_id(self: 'TelegramClient') -> typing.Optional[int]:
|
||||
"""
|
||||
Returns the ID of the logged-in user, if known.
|
||||
|
||||
This property is used in every update, and some like `updateLoginToken`
|
||||
occur prior to login, so it gracefully handles when no ID is known yet.
|
||||
"""
|
||||
return self._mb_entity_cache.self_id
|
||||
|
||||
async def is_bot(self: 'TelegramClient') -> bool:
|
||||
"""
|
||||
Return `True` if the signed-in user is a bot, `False` otherwise.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
if await client.is_bot():
|
||||
print('Beep')
|
||||
else:
|
||||
print('Hello')
|
||||
"""
|
||||
if self._mb_entity_cache.self_bot is None:
|
||||
await self.get_me(input_peer=True)
|
||||
|
||||
return self._mb_entity_cache.self_bot
|
||||
|
||||
async def is_user_authorized(self: 'TelegramClient') -> bool:
|
||||
"""
|
||||
Returns `True` if the user is authorized (logged in).
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
if not await client.is_user_authorized():
|
||||
await client.send_code_request(phone)
|
||||
code = input('enter code: ')
|
||||
await client.sign_in(phone, code)
|
||||
"""
|
||||
if self._authorized is None:
|
||||
try:
|
||||
# Any request that requires authorization will work
|
||||
await self(functions.updates.GetStateRequest())
|
||||
self._authorized = True
|
||||
except errors.RPCError:
|
||||
self._authorized = False
|
||||
|
||||
return self._authorized
|
||||
|
||||
async def get_entity(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntitiesLike') -> 'hints.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,
|
||||
and they will be efficiently fetched from the network.
|
||||
|
||||
Arguments
|
||||
entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
|
||||
If a username is given, **the username will be resolved** making
|
||||
an API call every time. Resolving usernames is an expensive
|
||||
operation and will start hitting flood waits around 50 usernames
|
||||
in a short period of time.
|
||||
|
||||
If you want to get the entity for a *cached* username, you should
|
||||
first `get_input_entity(username) <get_input_entity>` which will
|
||||
use the cache), and then use `get_entity` with the result of the
|
||||
previous call.
|
||||
|
||||
Similar limits apply to invite links, and you should use their
|
||||
ID instead.
|
||||
|
||||
Using phone numbers (from people in your contact list), exact
|
||||
names, integer IDs or :tl:`Peer` rely on a `get_input_entity`
|
||||
first, which in turn needs the entity to be in cache, unless
|
||||
a :tl:`InputPeer` was passed.
|
||||
|
||||
Unsupported types will raise ``TypeError``.
|
||||
|
||||
If the entity can't be found, ``ValueError`` will be raised.
|
||||
|
||||
Returns
|
||||
:tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the
|
||||
input entity. A list will be returned if more than one was given.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import utils
|
||||
|
||||
me = await client.get_entity('me')
|
||||
print(utils.get_display_name(me))
|
||||
|
||||
chat = await client.get_input_entity('username')
|
||||
async for message in client.iter_messages(chat):
|
||||
...
|
||||
|
||||
# Note that you could have used the username directly, but it's
|
||||
# good to use get_input_entity if you will reuse it a lot.
|
||||
async for message in client.iter_messages('username'):
|
||||
...
|
||||
|
||||
# Note that for this to work the phone number must be in your contacts
|
||||
some_id = await client.get_peer_id('+34123456789')
|
||||
"""
|
||||
single = not utils.is_list_like(entity)
|
||||
if single:
|
||||
entity = (entity,)
|
||||
|
||||
# Group input entities by string (resolve username),
|
||||
# input users (get users), input chat (get chats) and
|
||||
# input channels (get channels) to get the most entities
|
||||
# in the less amount of calls possible.
|
||||
inputs = []
|
||||
for x in entity:
|
||||
if isinstance(x, str):
|
||||
inputs.append(x)
|
||||
else:
|
||||
inputs.append(await self.get_input_entity(x))
|
||||
|
||||
lists = {
|
||||
helpers._EntityType.USER: [],
|
||||
helpers._EntityType.CHAT: [],
|
||||
helpers._EntityType.CHANNEL: [],
|
||||
}
|
||||
for x in inputs:
|
||||
try:
|
||||
lists[helpers._entity_type(x)].append(x)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
users = lists[helpers._EntityType.USER]
|
||||
chats = lists[helpers._EntityType.CHAT]
|
||||
channels = lists[helpers._EntityType.CHANNEL]
|
||||
if users:
|
||||
# GetUsersRequest has a limit of 200 per call
|
||||
tmp = []
|
||||
while users:
|
||||
curr, users = users[:200], users[200:]
|
||||
tmp.extend(await self(functions.users.GetUsersRequest(curr)))
|
||||
users = tmp
|
||||
if chats: # TODO Handle chats slice?
|
||||
chats = (await self(
|
||||
functions.messages.GetChatsRequest([x.chat_id for x in chats]))).chats
|
||||
if channels:
|
||||
channels = (await self(
|
||||
functions.channels.GetChannelsRequest(channels))).chats
|
||||
|
||||
# Merge users, chats and channels into a single dictionary
|
||||
id_entity = {
|
||||
# `get_input_entity` might've guessed the type from a non-marked ID,
|
||||
# so the only way to match that with the input is by not using marks here.
|
||||
utils.get_peer_id(x, add_mark=False): x
|
||||
for x in itertools.chain(users, chats, channels)
|
||||
}
|
||||
|
||||
# We could check saved usernames and put them into the users,
|
||||
# chats and channels list from before. While this would reduce
|
||||
# the amount of ResolveUsername calls, it would fail to catch
|
||||
# username changes.
|
||||
result = []
|
||||
for x in inputs:
|
||||
if isinstance(x, str):
|
||||
result.append(await self._get_entity_from_string(x))
|
||||
elif not isinstance(x, types.InputPeerSelf):
|
||||
result.append(id_entity[utils.get_peer_id(x, add_mark=False)])
|
||||
else:
|
||||
result.append(next(
|
||||
u for u in id_entity.values()
|
||||
if isinstance(u, types.User) and u.is_self
|
||||
))
|
||||
|
||||
return result[0] if single else result
|
||||
|
||||
async def get_input_entity(
|
||||
self: 'TelegramClient',
|
||||
peer: 'hints.EntityLike') -> 'types.TypeInputPeer':
|
||||
"""
|
||||
Turns the given entity into its input entity version.
|
||||
|
||||
Most requests use this kind of :tl:`InputPeer`, so this is the most
|
||||
suitable call to make for those cases. **Generally you should let the
|
||||
library do its job** and don't worry about getting the input entity
|
||||
first, but if you're going to use an entity often, consider making the
|
||||
call:
|
||||
|
||||
Arguments
|
||||
entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
|
||||
If a username or invite link is given, **the library will
|
||||
use the cache**. This means that it's possible to be using
|
||||
a username that *changed* or an old invite link (this only
|
||||
happens if an invite link for a small group chat is used
|
||||
after it was upgraded to a mega-group).
|
||||
|
||||
If the username or ID from the invite link is not found in
|
||||
the cache, it will be fetched. The same rules apply to phone
|
||||
numbers (``'+34 123456789'``) from people in your contact list.
|
||||
|
||||
If an exact name is given, it must be in the cache too. This
|
||||
is not reliable as different people can share the same name
|
||||
and which entity is returned is arbitrary, and should be used
|
||||
only for quick tests.
|
||||
|
||||
If a positive integer ID is given, the entity will be searched
|
||||
in cached users, chats or channels, without making any call.
|
||||
|
||||
If a negative integer ID is given, the entity will be searched
|
||||
exactly as either a chat (prefixed with ``-``) or as a channel
|
||||
(prefixed with ``-100``).
|
||||
|
||||
If a :tl:`Peer` is given, it will be searched exactly in the
|
||||
cache as either a user, chat or channel.
|
||||
|
||||
If the given object can be turned into an input entity directly,
|
||||
said operation will be done.
|
||||
|
||||
Unsupported types will raise ``TypeError``.
|
||||
|
||||
If the entity can't be found, ``ValueError`` will be raised.
|
||||
|
||||
Returns
|
||||
:tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`
|
||||
or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``.
|
||||
|
||||
If you need to get the ID of yourself, you should use
|
||||
`get_me` with ``input_peer=True``) instead.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# If you're going to use "username" often in your code
|
||||
# (make a lot of calls), consider getting its input entity
|
||||
# once, and then using the "user" everywhere instead.
|
||||
user = await client.get_input_entity('username')
|
||||
|
||||
# The same applies to IDs, chats or channels.
|
||||
chat = await client.get_input_entity(-123456789)
|
||||
"""
|
||||
# Short-circuit if the input parameter directly maps to an InputPeer
|
||||
try:
|
||||
return utils.get_input_peer(peer)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Next in priority is having a peer (or its ID) cached in-memory
|
||||
try:
|
||||
# 0x2d45687 == crc32(b'Peer')
|
||||
if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687:
|
||||
return self._mb_entity_cache.get(utils.get_peer_id(peer, add_mark=False))._as_input_peer()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Then come known strings that take precedence
|
||||
if peer in ('me', 'self'):
|
||||
return types.InputPeerSelf()
|
||||
|
||||
# No InputPeer, cached peer, or known string. Fetch from disk cache
|
||||
try:
|
||||
return self.session.get_input_entity(peer)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Only network left to try
|
||||
if isinstance(peer, str):
|
||||
return utils.get_input_peer(
|
||||
await self._get_entity_from_string(peer))
|
||||
|
||||
# If we're a bot and the user has messaged us privately users.getUsers
|
||||
# will work with access_hash = 0. Similar for channels.getChannels.
|
||||
# If we're not a bot but the user is in our contacts, it seems to work
|
||||
# regardless. These are the only two special-cased requests.
|
||||
peer = utils.get_peer(peer)
|
||||
if isinstance(peer, types.PeerUser):
|
||||
users = await self(functions.users.GetUsersRequest([
|
||||
types.InputUser(peer.user_id, access_hash=0)]))
|
||||
if users and not isinstance(users[0], types.UserEmpty):
|
||||
# If the user passed a valid ID they expect to work for
|
||||
# channels but would be valid for users, we get UserEmpty.
|
||||
# Avoid returning the invalid empty input peer for that.
|
||||
#
|
||||
# We *could* try to guess if it's a channel first, and if
|
||||
# it's not, work as a chat and try to validate it through
|
||||
# another request, but that becomes too much work.
|
||||
return utils.get_input_peer(users[0])
|
||||
elif isinstance(peer, types.PeerChat):
|
||||
return types.InputPeerChat(peer.chat_id)
|
||||
elif isinstance(peer, types.PeerChannel):
|
||||
try:
|
||||
channels = await self(functions.channels.GetChannelsRequest([
|
||||
types.InputChannel(peer.channel_id, access_hash=0)]))
|
||||
return utils.get_input_peer(channels.chats[0])
|
||||
except errors.ChannelInvalidError:
|
||||
pass
|
||||
|
||||
raise ValueError(
|
||||
'Could not find the input entity for {} ({}). Please read https://'
|
||||
'docs.telethon.dev/en/stable/concepts/entities.html to'
|
||||
' find out more details.'
|
||||
.format(peer, type(peer).__name__)
|
||||
)
|
||||
|
||||
async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'):
|
||||
i, cls = utils.resolve_id(await self.get_peer_id(peer))
|
||||
return cls(i)
|
||||
|
||||
async def get_peer_id(
|
||||
self: 'TelegramClient',
|
||||
peer: 'hints.EntityLike',
|
||||
add_mark: bool = True) -> int:
|
||||
"""
|
||||
Gets the ID for the given entity.
|
||||
|
||||
This method needs to be ``async`` because `peer` supports usernames,
|
||||
invite-links, phone numbers (from people in your contact list), etc.
|
||||
|
||||
If ``add_mark is False``, then a positive ID will be returned
|
||||
instead. By default, bot-API style IDs (signed) are returned.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
print(await client.get_peer_id('me'))
|
||||
"""
|
||||
if isinstance(peer, int):
|
||||
return utils.get_peer_id(peer, add_mark=add_mark)
|
||||
|
||||
try:
|
||||
if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6):
|
||||
# 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer'
|
||||
peer = await self.get_input_entity(peer)
|
||||
except AttributeError:
|
||||
peer = await self.get_input_entity(peer)
|
||||
|
||||
if isinstance(peer, types.InputPeerSelf):
|
||||
peer = await self.get_me(input_peer=True)
|
||||
|
||||
return utils.get_peer_id(peer, add_mark=add_mark)
|
||||
|
||||
# endregion
|
||||
|
||||
# region Private methods
|
||||
|
||||
async def _get_entity_from_string(self: 'TelegramClient', string):
|
||||
"""
|
||||
Gets a full entity from the given string, which may be a phone or
|
||||
a username, and processes all the found entities on the session.
|
||||
The string may also be a user link, or a channel/chat invite link.
|
||||
|
||||
This method has the side effect of adding the found users to the
|
||||
session database, so it can be queried later without API calls,
|
||||
if this option is enabled on the session.
|
||||
|
||||
Returns the found entity, or raises TypeError if not found.
|
||||
"""
|
||||
phone = utils.parse_phone(string)
|
||||
if phone:
|
||||
try:
|
||||
for user in (await self(
|
||||
functions.contacts.GetContactsRequest(0))).users:
|
||||
if user.phone == phone:
|
||||
return user
|
||||
except errors.BotMethodInvalidError:
|
||||
raise ValueError('Cannot get entity by phone number as a '
|
||||
'bot (try using integer IDs, not strings)')
|
||||
elif string.lower() in ('me', 'self'):
|
||||
return await self.get_me()
|
||||
else:
|
||||
username, is_join_chat = utils.parse_username(string)
|
||||
if is_join_chat:
|
||||
invite = await self(
|
||||
functions.messages.CheckChatInviteRequest(username))
|
||||
|
||||
if isinstance(invite, types.ChatInvite):
|
||||
raise ValueError(
|
||||
'Cannot get entity from a channel (or group) '
|
||||
'that you are not part of. Join the group and retry'
|
||||
)
|
||||
elif isinstance(invite, types.ChatInviteAlready):
|
||||
return invite.chat
|
||||
elif username:
|
||||
try:
|
||||
result = await self(
|
||||
functions.contacts.ResolveUsernameRequest(username))
|
||||
except errors.UsernameNotOccupiedError as e:
|
||||
raise ValueError('No user has "{}" as username'
|
||||
.format(username)) from e
|
||||
|
||||
try:
|
||||
pid = utils.get_peer_id(result.peer, add_mark=False)
|
||||
if isinstance(result.peer, types.PeerUser):
|
||||
return next(x for x in result.users if x.id == pid)
|
||||
else:
|
||||
return next(x for x in result.chats if x.id == pid)
|
||||
except StopIteration:
|
||||
pass
|
||||
try:
|
||||
# Nobody with this username, maybe it's an exact name/title
|
||||
return await self.get_entity(
|
||||
self.session.get_input_entity(string))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
raise ValueError(
|
||||
'Cannot find any entity corresponding to "{}"'.format(string)
|
||||
)
|
||||
|
||||
async def _get_input_dialog(self: 'TelegramClient', dialog):
|
||||
"""
|
||||
Returns a :tl:`InputDialogPeer`. This is a bit tricky because
|
||||
it may or not need access to the client to convert what's given
|
||||
into an input entity.
|
||||
"""
|
||||
try:
|
||||
if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer')
|
||||
dialog.peer = await self.get_input_entity(dialog.peer)
|
||||
return dialog
|
||||
elif dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
|
||||
return types.InputDialogPeer(dialog)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return types.InputDialogPeer(await self.get_input_entity(dialog))
|
||||
|
||||
async def _get_input_notify(self: 'TelegramClient', notify):
|
||||
"""
|
||||
Returns a :tl:`InputNotifyPeer`. This is a bit tricky because
|
||||
it may or not need access to the client to convert what's given
|
||||
into an input entity.
|
||||
"""
|
||||
try:
|
||||
if notify.SUBCLASS_OF_ID == 0x58981615:
|
||||
if isinstance(notify, types.InputNotifyPeer):
|
||||
notify.peer = await self.get_input_entity(notify.peer)
|
||||
return notify
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return types.InputNotifyPeer(await self.get_input_entity(notify))
|
||||
|
||||
# endregion
|
|
@ -1,10 +0,0 @@
|
|||
"""
|
||||
This module contains several utilities regarding cryptographic purposes,
|
||||
such as the AES IGE mode used by Telegram, the authorization key bound with
|
||||
their data centers, and so on.
|
||||
"""
|
||||
from .aes import AES
|
||||
from .aesctr import AESModeCTR
|
||||
from .authkey import AuthKey
|
||||
from .factorization import Factorization
|
||||
from .cdndecrypter import CdnDecrypter
|
|
@ -1,111 +0,0 @@
|
|||
"""
|
||||
AES IGE implementation in Python.
|
||||
|
||||
If available, cryptg will be used instead, otherwise
|
||||
if available, libssl will be used instead, otherwise
|
||||
the Python implementation will be used.
|
||||
"""
|
||||
import os
|
||||
import pyaes
|
||||
import logging
|
||||
from . import libssl
|
||||
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
try:
|
||||
import cryptg
|
||||
__log__.info('cryptg detected, it will be used for encryption')
|
||||
except ImportError:
|
||||
cryptg = None
|
||||
if libssl.encrypt_ige and libssl.decrypt_ige:
|
||||
__log__.info('libssl detected, it will be used for encryption')
|
||||
else:
|
||||
__log__.info('cryptg module not installed and libssl not found, '
|
||||
'falling back to (slower) Python encryption')
|
||||
|
||||
|
||||
class AES:
|
||||
"""
|
||||
Class that servers as an interface to encrypt and decrypt
|
||||
text through the AES IGE mode.
|
||||
"""
|
||||
@staticmethod
|
||||
def decrypt_ige(cipher_text, key, iv):
|
||||
"""
|
||||
Decrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
if cryptg:
|
||||
return cryptg.decrypt_ige(cipher_text, key, iv)
|
||||
if libssl.decrypt_ige:
|
||||
return libssl.decrypt_ige(cipher_text, key, iv)
|
||||
|
||||
iv1 = iv[:len(iv) // 2]
|
||||
iv2 = iv[len(iv) // 2:]
|
||||
|
||||
aes = pyaes.AES(key)
|
||||
|
||||
plain_text = []
|
||||
blocks_count = len(cipher_text) // 16
|
||||
|
||||
cipher_text_block = [0] * 16
|
||||
for block_index in range(blocks_count):
|
||||
for i in range(16):
|
||||
cipher_text_block[i] = \
|
||||
cipher_text[block_index * 16 + i] ^ iv2[i]
|
||||
|
||||
plain_text_block = aes.decrypt(cipher_text_block)
|
||||
|
||||
for i in range(16):
|
||||
plain_text_block[i] ^= iv1[i]
|
||||
|
||||
iv1 = cipher_text[block_index * 16:block_index * 16 + 16]
|
||||
iv2 = plain_text_block
|
||||
|
||||
plain_text.extend(plain_text_block)
|
||||
|
||||
return bytes(plain_text)
|
||||
|
||||
@staticmethod
|
||||
def encrypt_ige(plain_text, key, iv):
|
||||
"""
|
||||
Encrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
padding = len(plain_text) % 16
|
||||
if padding:
|
||||
plain_text += os.urandom(16 - padding)
|
||||
|
||||
if cryptg:
|
||||
return cryptg.encrypt_ige(plain_text, key, iv)
|
||||
if libssl.encrypt_ige:
|
||||
return libssl.encrypt_ige(plain_text, key, iv)
|
||||
|
||||
iv1 = iv[:len(iv) // 2]
|
||||
iv2 = iv[len(iv) // 2:]
|
||||
|
||||
aes = pyaes.AES(key)
|
||||
|
||||
cipher_text = []
|
||||
blocks_count = len(plain_text) // 16
|
||||
|
||||
for block_index in range(blocks_count):
|
||||
plain_text_block = list(
|
||||
plain_text[block_index * 16:block_index * 16 + 16]
|
||||
)
|
||||
for i in range(16):
|
||||
plain_text_block[i] ^= iv1[i]
|
||||
|
||||
cipher_text_block = aes.encrypt(plain_text_block)
|
||||
|
||||
for i in range(16):
|
||||
cipher_text_block[i] ^= iv2[i]
|
||||
|
||||
iv1 = cipher_text_block
|
||||
iv2 = plain_text[block_index * 16:block_index * 16 + 16]
|
||||
|
||||
cipher_text.extend(cipher_text_block)
|
||||
|
||||
return bytes(cipher_text)
|
|
@ -1,42 +0,0 @@
|
|||
"""
|
||||
This module holds the AESModeCTR wrapper class.
|
||||
"""
|
||||
import pyaes
|
||||
|
||||
|
||||
class AESModeCTR:
|
||||
"""Wrapper around pyaes.AESModeOfOperationCTR mode with custom IV"""
|
||||
# TODO Maybe make a pull request to pyaes to support iv on CTR
|
||||
|
||||
def __init__(self, key, iv):
|
||||
"""
|
||||
Initializes the AES CTR mode with the given key/iv pair.
|
||||
|
||||
:param key: the key to be used as bytes.
|
||||
:param iv: the bytes initialization vector. Must have a length of 16.
|
||||
"""
|
||||
# TODO Use libssl if available
|
||||
assert isinstance(key, bytes)
|
||||
self._aes = pyaes.AESModeOfOperationCTR(key)
|
||||
|
||||
assert isinstance(iv, bytes)
|
||||
assert len(iv) == 16
|
||||
self._aes._counter._counter = list(iv)
|
||||
|
||||
def encrypt(self, data):
|
||||
"""
|
||||
Encrypts the given plain text through AES CTR.
|
||||
|
||||
:param data: the plain text to be encrypted.
|
||||
:return: the encrypted cipher text.
|
||||
"""
|
||||
return self._aes.encrypt(data)
|
||||
|
||||
def decrypt(self, data):
|
||||
"""
|
||||
Decrypts the given cipher text through AES CTR
|
||||
|
||||
:param data: the cipher text to be decrypted.
|
||||
:return: the decrypted plain text.
|
||||
"""
|
||||
return self._aes.decrypt(data)
|
|
@ -1,63 +0,0 @@
|
|||
"""
|
||||
This module holds the AuthKey class.
|
||||
"""
|
||||
import struct
|
||||
from hashlib import sha1
|
||||
|
||||
from ..extensions import BinaryReader
|
||||
|
||||
|
||||
class AuthKey:
|
||||
"""
|
||||
Represents an authorization key, used to encrypt and decrypt
|
||||
messages sent to Telegram's data centers.
|
||||
"""
|
||||
def __init__(self, data):
|
||||
"""
|
||||
Initializes a new authorization key.
|
||||
|
||||
:param data: the data in bytes that represent this auth key.
|
||||
"""
|
||||
self.key = data
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return self._key
|
||||
|
||||
@key.setter
|
||||
def key(self, value):
|
||||
if not value:
|
||||
self._key = self.aux_hash = self.key_id = None
|
||||
return
|
||||
|
||||
if isinstance(value, type(self)):
|
||||
self._key, self.aux_hash, self.key_id = \
|
||||
value._key, value.aux_hash, value.key_id
|
||||
return
|
||||
|
||||
self._key = value
|
||||
with BinaryReader(sha1(self._key).digest()) as reader:
|
||||
self.aux_hash = reader.read_long(signed=False)
|
||||
reader.read(4)
|
||||
self.key_id = reader.read_long(signed=False)
|
||||
|
||||
# TODO This doesn't really fit here, it's only used in authentication
|
||||
def calc_new_nonce_hash(self, new_nonce, number):
|
||||
"""
|
||||
Calculates the new nonce hash based on the current attributes.
|
||||
|
||||
:param new_nonce: the new nonce to be hashed.
|
||||
:param number: number to prepend before the hash.
|
||||
:return: the hash for the given new nonce.
|
||||
"""
|
||||
new_nonce = new_nonce.to_bytes(32, 'little', signed=True)
|
||||
data = new_nonce + struct.pack('<BQ', number, self.aux_hash)
|
||||
|
||||
# Calculates the message key from the given data
|
||||
return int.from_bytes(sha1(data).digest()[4:20], 'little', signed=True)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._key)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, type(self)) and other.key == self._key
|
|
@ -1,105 +0,0 @@
|
|||
"""
|
||||
This module holds the CdnDecrypter utility class.
|
||||
"""
|
||||
from hashlib import sha256
|
||||
|
||||
from ..tl.functions.upload import GetCdnFileRequest, ReuploadCdnFileRequest
|
||||
from ..tl.types.upload import CdnFileReuploadNeeded, CdnFile
|
||||
from ..crypto import AESModeCTR
|
||||
from ..errors import CdnFileTamperedError
|
||||
|
||||
|
||||
class CdnDecrypter:
|
||||
"""
|
||||
Used when downloading a file results in a 'FileCdnRedirect' to
|
||||
both prepare the redirect, decrypt the file as it downloads, and
|
||||
ensure the file hasn't been tampered. https://core.telegram.org/cdn
|
||||
"""
|
||||
def __init__(self, cdn_client, file_token, cdn_aes, cdn_file_hashes):
|
||||
"""
|
||||
Initializes the CDN decrypter.
|
||||
|
||||
:param cdn_client: a client connected to a CDN.
|
||||
:param file_token: the token of the file to be used.
|
||||
:param cdn_aes: the AES CTR used to decrypt the file.
|
||||
:param cdn_file_hashes: the hashes the decrypted file must match.
|
||||
"""
|
||||
self.client = cdn_client
|
||||
self.file_token = file_token
|
||||
self.cdn_aes = cdn_aes
|
||||
self.cdn_file_hashes = cdn_file_hashes
|
||||
|
||||
@staticmethod
|
||||
async def prepare_decrypter(client, cdn_client, cdn_redirect):
|
||||
"""
|
||||
Prepares a new CDN decrypter.
|
||||
|
||||
:param client: a TelegramClient connected to the main servers.
|
||||
:param cdn_client: a new client connected to the CDN.
|
||||
:param cdn_redirect: the redirect file object that caused this call.
|
||||
:return: (CdnDecrypter, first chunk file data)
|
||||
"""
|
||||
cdn_aes = AESModeCTR(
|
||||
key=cdn_redirect.encryption_key,
|
||||
# 12 first bytes of the IV..4 bytes of the offset (0, big endian)
|
||||
iv=cdn_redirect.encryption_iv[:12] + bytes(4)
|
||||
)
|
||||
|
||||
# We assume that cdn_redirect.cdn_file_hashes are ordered by offset,
|
||||
# and that there will be enough of these to retrieve the whole file.
|
||||
decrypter = CdnDecrypter(
|
||||
cdn_client, cdn_redirect.file_token,
|
||||
cdn_aes, cdn_redirect.cdn_file_hashes
|
||||
)
|
||||
|
||||
cdn_file = await 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(
|
||||
file_token=cdn_redirect.file_token,
|
||||
request_token=cdn_file.request_token
|
||||
))
|
||||
|
||||
# We want to always return a valid upload.CdnFile
|
||||
cdn_file = decrypter.get_file()
|
||||
else:
|
||||
cdn_file.bytes = decrypter.cdn_aes.encrypt(cdn_file.bytes)
|
||||
cdn_hash = decrypter.cdn_file_hashes.pop(0)
|
||||
decrypter.check(cdn_file.bytes, cdn_hash)
|
||||
|
||||
return decrypter, cdn_file
|
||||
|
||||
def get_file(self):
|
||||
"""
|
||||
Calls GetCdnFileRequest and decrypts its bytes.
|
||||
Also ensures that the file hasn't been tampered.
|
||||
|
||||
:return: the CdnFile result.
|
||||
"""
|
||||
if self.cdn_file_hashes:
|
||||
cdn_hash = self.cdn_file_hashes.pop(0)
|
||||
cdn_file = self.client(GetCdnFileRequest(
|
||||
self.file_token, cdn_hash.offset, cdn_hash.limit
|
||||
))
|
||||
cdn_file.bytes = self.cdn_aes.encrypt(cdn_file.bytes)
|
||||
self.check(cdn_file.bytes, cdn_hash)
|
||||
else:
|
||||
cdn_file = CdnFile(bytes(0))
|
||||
|
||||
return cdn_file
|
||||
|
||||
@staticmethod
|
||||
def check(data, cdn_hash):
|
||||
"""
|
||||
Checks the integrity of the given data.
|
||||
Raises CdnFileTamperedError if the integrity check fails.
|
||||
|
||||
:param data: the data to be hashed.
|
||||
:param cdn_hash: the expected hash.
|
||||
"""
|
||||
if sha256(data).digest() != cdn_hash.hash:
|
||||
raise CdnFileTamperedError()
|
|
@ -1,67 +0,0 @@
|
|||
"""
|
||||
This module holds a fast Factorization class.
|
||||
"""
|
||||
from random import randint
|
||||
|
||||
|
||||
class Factorization:
|
||||
"""
|
||||
Simple module to factorize large numbers really quickly.
|
||||
"""
|
||||
@classmethod
|
||||
def factorize(cls, pq):
|
||||
"""
|
||||
Factorizes the given large integer.
|
||||
|
||||
Implementation from https://comeoncodeon.wordpress.com/2010/09/18/pollard-rho-brent-integer-factorization/.
|
||||
|
||||
:param pq: the prime pair pq.
|
||||
:return: a tuple containing the two factors p and q.
|
||||
"""
|
||||
if pq % 2 == 0:
|
||||
return 2, pq // 2
|
||||
|
||||
y, c, m = randint(1, pq - 1), randint(1, pq - 1), randint(1, pq - 1)
|
||||
g = r = q = 1
|
||||
x = ys = 0
|
||||
|
||||
while g == 1:
|
||||
x = y
|
||||
for i in range(r):
|
||||
y = (pow(y, 2, pq) + c) % pq
|
||||
|
||||
k = 0
|
||||
while k < r and g == 1:
|
||||
ys = y
|
||||
for i in range(min(m, r - k)):
|
||||
y = (pow(y, 2, pq) + c) % pq
|
||||
q = q * (abs(x - y)) % pq
|
||||
|
||||
g = cls.gcd(q, pq)
|
||||
k += m
|
||||
|
||||
r *= 2
|
||||
|
||||
if g == pq:
|
||||
while True:
|
||||
ys = (pow(ys, 2, pq) + c) % pq
|
||||
g = cls.gcd(abs(x - ys), pq)
|
||||
if g > 1:
|
||||
break
|
||||
|
||||
p, q = g, pq // g
|
||||
return (p, q) if p < q else (q, p)
|
||||
|
||||
@staticmethod
|
||||
def gcd(a, b):
|
||||
"""
|
||||
Calculates the Greatest Common Divisor.
|
||||
|
||||
:param a: the first number.
|
||||
:param b: the second number.
|
||||
:return: GCD(a, b)
|
||||
"""
|
||||
while b:
|
||||
a, b = b, a % b
|
||||
|
||||
return a
|
|
@ -1,140 +0,0 @@
|
|||
"""
|
||||
Helper module around the system's libssl library if available for IGE mode.
|
||||
"""
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import platform
|
||||
import sys
|
||||
try:
|
||||
import ctypes.macholib.dyld
|
||||
except ImportError:
|
||||
pass
|
||||
import logging
|
||||
import os
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _find_ssl_lib():
|
||||
lib = ctypes.util.find_library('ssl')
|
||||
# macOS 10.15 segfaults on unversioned crypto libraries.
|
||||
# We therefore pin the current stable version here
|
||||
# Credit for fix goes to Sarah Harvey (@worldwise001)
|
||||
# https://www.shh.sh/2020/01/04/python-abort-trap-6.html
|
||||
if sys.platform == 'darwin':
|
||||
release, _version_info, _machine = platform.mac_ver()
|
||||
ver, major, *_ = release.split('.')
|
||||
# macOS 10.14 "mojave" is the last known major release
|
||||
# to support unversioned libssl.dylib. Anything above
|
||||
# needs specific versions
|
||||
if int(ver) > 10 or int(ver) == 10 and int(major) > 14:
|
||||
lib = (
|
||||
ctypes.util.find_library('libssl.46') or
|
||||
ctypes.util.find_library('libssl.44') or
|
||||
ctypes.util.find_library('libssl.42')
|
||||
)
|
||||
if not lib:
|
||||
raise OSError('no library called "ssl" found')
|
||||
|
||||
# First, let ctypes try to handle it itself.
|
||||
try:
|
||||
libssl = ctypes.cdll.LoadLibrary(lib)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
return libssl
|
||||
|
||||
# This is a best-effort attempt at finding the full real path of lib.
|
||||
#
|
||||
# Unfortunately ctypes doesn't tell us *where* it finds the library,
|
||||
# so we have to do that ourselves.
|
||||
try:
|
||||
# This is not documented, so it could fail. Be on the safe side.
|
||||
paths = ctypes.macholib.dyld.DEFAULT_LIBRARY_FALLBACK
|
||||
except AttributeError:
|
||||
paths = [
|
||||
os.path.expanduser("~/lib"),
|
||||
"/usr/local/lib",
|
||||
"/lib",
|
||||
"/usr/lib",
|
||||
]
|
||||
|
||||
for path in paths:
|
||||
if os.path.isdir(path):
|
||||
for root, _, files in os.walk(path):
|
||||
if lib in files:
|
||||
# Manually follow symbolic links on *nix systems.
|
||||
# Fix for https://github.com/LonamiWebs/Telethon/issues/1167
|
||||
lib = os.path.realpath(os.path.join(root, lib))
|
||||
return ctypes.cdll.LoadLibrary(lib)
|
||||
else:
|
||||
raise OSError('no absolute path for "%s" and cannot load by name' % lib)
|
||||
|
||||
|
||||
try:
|
||||
_libssl = _find_ssl_lib()
|
||||
except OSError as e:
|
||||
# See https://github.com/LonamiWebs/Telethon/issues/1167
|
||||
# Sometimes `find_library` returns improper filenames.
|
||||
__log__.info('Failed to load SSL library: %s (%s)', type(e), e)
|
||||
_libssl = None
|
||||
|
||||
if not _libssl:
|
||||
decrypt_ige = None
|
||||
encrypt_ige = None
|
||||
else:
|
||||
# https://github.com/openssl/openssl/blob/master/include/openssl/aes.h
|
||||
AES_ENCRYPT = ctypes.c_int(1)
|
||||
AES_DECRYPT = ctypes.c_int(0)
|
||||
AES_MAXNR = 14
|
||||
|
||||
class AES_KEY(ctypes.Structure):
|
||||
"""Helper class representing an AES key"""
|
||||
_fields_ = [
|
||||
('rd_key', ctypes.c_uint32 * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', ctypes.c_uint),
|
||||
]
|
||||
|
||||
def decrypt_ige(cipher_text, key, iv):
|
||||
aes_key = AES_KEY()
|
||||
key_len = ctypes.c_int(8 * len(key))
|
||||
key = (ctypes.c_ubyte * len(key))(*key)
|
||||
iv = (ctypes.c_ubyte * len(iv))(*iv)
|
||||
|
||||
in_len = ctypes.c_size_t(len(cipher_text))
|
||||
in_ptr = (ctypes.c_ubyte * len(cipher_text))(*cipher_text)
|
||||
out_ptr = (ctypes.c_ubyte * len(cipher_text))()
|
||||
|
||||
_libssl.AES_set_decrypt_key(key, key_len, ctypes.byref(aes_key))
|
||||
_libssl.AES_ige_encrypt(
|
||||
ctypes.byref(in_ptr),
|
||||
ctypes.byref(out_ptr),
|
||||
in_len,
|
||||
ctypes.byref(aes_key),
|
||||
ctypes.byref(iv),
|
||||
AES_DECRYPT
|
||||
)
|
||||
|
||||
return bytes(out_ptr)
|
||||
|
||||
def encrypt_ige(plain_text, key, iv):
|
||||
aes_key = AES_KEY()
|
||||
key_len = ctypes.c_int(8 * len(key))
|
||||
key = (ctypes.c_ubyte * len(key))(*key)
|
||||
iv = (ctypes.c_ubyte * len(iv))(*iv)
|
||||
|
||||
in_len = ctypes.c_size_t(len(plain_text))
|
||||
in_ptr = (ctypes.c_ubyte * len(plain_text))(*plain_text)
|
||||
out_ptr = (ctypes.c_ubyte * len(plain_text))()
|
||||
|
||||
_libssl.AES_set_encrypt_key(key, key_len, ctypes.byref(aes_key))
|
||||
_libssl.AES_ige_encrypt(
|
||||
ctypes.byref(in_ptr),
|
||||
ctypes.byref(out_ptr),
|
||||
in_len,
|
||||
ctypes.byref(aes_key),
|
||||
ctypes.byref(iv),
|
||||
AES_ENCRYPT
|
||||
)
|
||||
|
||||
return bytes(out_ptr)
|
|
@ -1,165 +0,0 @@
|
|||
"""
|
||||
This module holds several utilities regarding RSA and server fingerprints.
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
from hashlib import sha1
|
||||
try:
|
||||
import rsa
|
||||
import rsa.core
|
||||
except ImportError:
|
||||
rsa = None
|
||||
raise ImportError('Missing module "rsa", please install via pip.')
|
||||
|
||||
from ..tl import TLObject
|
||||
|
||||
|
||||
# {fingerprint: (Crypto.PublicKey.RSA._RSAobj, old)} dictionary
|
||||
_server_keys = {}
|
||||
|
||||
|
||||
def get_byte_array(integer):
|
||||
"""Return the variable length bytes corresponding to the given int"""
|
||||
# Operate in big endian (unlike most of Telegram API) since:
|
||||
# > "...pq is a representation of a natural number
|
||||
# (in binary *big endian* format)..."
|
||||
# > "...current value of dh_prime equals
|
||||
# (in *big-endian* byte order)..."
|
||||
# Reference: https://core.telegram.org/mtproto/auth_key
|
||||
return int.to_bytes(
|
||||
integer,
|
||||
(integer.bit_length() + 8 - 1) // 8, # 8 bits per byte,
|
||||
byteorder='big',
|
||||
signed=False
|
||||
)
|
||||
|
||||
|
||||
def _compute_fingerprint(key):
|
||||
"""
|
||||
Given a RSA key, computes its fingerprint like Telegram does.
|
||||
|
||||
:param key: the Crypto.RSA key.
|
||||
:return: its 8-bytes-long fingerprint.
|
||||
"""
|
||||
n = TLObject.serialize_bytes(get_byte_array(key.n))
|
||||
e = TLObject.serialize_bytes(get_byte_array(key.e))
|
||||
# Telegram uses the last 8 bytes as the fingerprint
|
||||
return struct.unpack('<q', sha1(n + e).digest()[-8:])[0]
|
||||
|
||||
|
||||
def add_key(pub, *, old):
|
||||
"""Adds a new public key to be used when encrypting new data is needed"""
|
||||
global _server_keys
|
||||
key = rsa.PublicKey.load_pkcs1(pub)
|
||||
_server_keys[_compute_fingerprint(key)] = (key, old)
|
||||
|
||||
|
||||
def encrypt(fingerprint, data, *, use_old=False):
|
||||
"""
|
||||
Encrypts the given data known the fingerprint to be used
|
||||
in the way Telegram requires us to do so (sha1(data) + data + padding)
|
||||
|
||||
:param fingerprint: the fingerprint of the RSA key.
|
||||
:param data: the data to be encrypted.
|
||||
:param use_old: whether old keys should be used.
|
||||
:return:
|
||||
the cipher text, or None if no key matching this fingerprint is found.
|
||||
"""
|
||||
global _server_keys
|
||||
key, old = _server_keys.get(fingerprint, [None, None])
|
||||
if (not key) or (old and not use_old):
|
||||
return None
|
||||
|
||||
# len(sha1.digest) is always 20, so we're left with 255 - 20 - x padding
|
||||
to_encrypt = sha1(data).digest() + data + os.urandom(235 - len(data))
|
||||
|
||||
# rsa module rsa.encrypt adds 11 bits for padding which we don't want
|
||||
# rsa module uses rsa.transform.bytes2int(to_encrypt), easier way:
|
||||
payload = int.from_bytes(to_encrypt, 'big')
|
||||
encrypted = rsa.core.encrypt_int(payload, key.e, key.n)
|
||||
# rsa module uses transform.int2bytes(encrypted, keylength), easier:
|
||||
block = encrypted.to_bytes(256, 'big')
|
||||
return block
|
||||
|
||||
|
||||
# Add default keys
|
||||
# https://github.com/DrKLO/Telegram/blob/a724d96e9c008b609fe188d122aa2922e40de5fc/TMessagesProj/jni/tgnet/Handshake.cpp#L356-L436
|
||||
for pub in (
|
||||
'''-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBCgKCAQEAruw2yP/BCcsJliRoW5eBVBVle9dtjJw+OYED160Wybum9SXtBBLX
|
||||
riwt4rROd9csv0t0OHCaTmRqBcQ0J8fxhN6/cpR1GWgOZRUAiQxoMnlt0R93LCX/
|
||||
j1dnVa/gVbCjdSxpbrfY2g2L4frzjJvdl84Kd9ORYjDEAyFnEA7dD556OptgLQQ2
|
||||
e2iVNq8NZLYTzLp5YpOdO1doK+ttrltggTCy5SrKeLoCPPbOgGsdxJxyz5KKcZnS
|
||||
Lj16yE5HvJQn0CNpRdENvRUXe6tBP78O39oJ8BTHp9oIjd6XWXAsp2CvK45Ol8wF
|
||||
XGF710w9lwCGNbmNxNYhtIkdqfsEcwR5JwIDAQAB
|
||||
-----END RSA PUBLIC KEY-----''',
|
||||
|
||||
'''-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBCgKCAQEAvfLHfYH2r9R70w8prHblWt/nDkh+XkgpflqQVcnAfSuTtO05lNPs
|
||||
pQmL8Y2XjVT4t8cT6xAkdgfmmvnvRPOOKPi0OfJXoRVylFzAQG/j83u5K3kRLbae
|
||||
7fLccVhKZhY46lvsueI1hQdLgNV9n1cQ3TDS2pQOCtovG4eDl9wacrXOJTG2990V
|
||||
jgnIKNA0UMoP+KF03qzryqIt3oTvZq03DyWdGK+AZjgBLaDKSnC6qD2cFY81UryR
|
||||
WOab8zKkWAnhw2kFpcqhI0jdV5QaSCExvnsjVaX0Y1N0870931/5Jb9ICe4nweZ9
|
||||
kSDF/gip3kWLG0o8XQpChDfyvsqB9OLV/wIDAQAB
|
||||
-----END RSA PUBLIC KEY-----''',
|
||||
|
||||
'''-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBCgKCAQEAs/ditzm+mPND6xkhzwFIz6J/968CtkcSE/7Z2qAJiXbmZ3UDJPGr
|
||||
zqTDHkO30R8VeRM/Kz2f4nR05GIFiITl4bEjvpy7xqRDspJcCFIOcyXm8abVDhF+
|
||||
th6knSU0yLtNKuQVP6voMrnt9MV1X92LGZQLgdHZbPQz0Z5qIpaKhdyA8DEvWWvS
|
||||
Uwwc+yi1/gGaybwlzZwqXYoPOhwMebzKUk0xW14htcJrRrq+PXXQbRzTMynseCoP
|
||||
Ioke0dtCodbA3qQxQovE16q9zz4Otv2k4j63cz53J+mhkVWAeWxVGI0lltJmWtEY
|
||||
K6er8VqqWot3nqmWMXogrgRLggv/NbbooQIDAQAB
|
||||
-----END RSA PUBLIC KEY-----''',
|
||||
|
||||
'''-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBCgKCAQEAvmpxVY7ld/8DAjz6F6q05shjg8/4p6047bn6/m8yPy1RBsvIyvuD
|
||||
uGnP/RzPEhzXQ9UJ5Ynmh2XJZgHoE9xbnfxL5BXHplJhMtADXKM9bWB11PU1Eioc
|
||||
3+AXBB8QiNFBn2XI5UkO5hPhbb9mJpjA9Uhw8EdfqJP8QetVsI/xrCEbwEXe0xvi
|
||||
fRLJbY08/Gp66KpQvy7g8w7VB8wlgePexW3pT13Ap6vuC+mQuJPyiHvSxjEKHgqe
|
||||
Pji9NP3tJUFQjcECqcm0yV7/2d0t/pbCm+ZH1sadZspQCEPPrtbkQBlvHb4OLiIW
|
||||
PGHKSMeRFvp3IWcmdJqXahxLCUS1Eh6MAQIDAQAB
|
||||
-----END RSA PUBLIC KEY-----''',
|
||||
|
||||
):
|
||||
add_key(pub, old=False)
|
||||
|
||||
|
||||
for pub in (
|
||||
'''-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
|
||||
lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS
|
||||
an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw
|
||||
Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+
|
||||
8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n
|
||||
Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB
|
||||
-----END RSA PUBLIC KEY-----''',
|
||||
|
||||
'''-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBCgKCAQEAxq7aeLAqJR20tkQQMfRn+ocfrtMlJsQ2Uksfs7Xcoo77jAid0bRt
|
||||
ksiVmT2HEIJUlRxfABoPBV8wY9zRTUMaMA654pUX41mhyVN+XoerGxFvrs9dF1Ru
|
||||
vCHbI02dM2ppPvyytvvMoefRoL5BTcpAihFgm5xCaakgsJ/tH5oVl74CdhQw8J5L
|
||||
xI/K++KJBUyZ26Uba1632cOiq05JBUW0Z2vWIOk4BLysk7+U9z+SxynKiZR3/xdi
|
||||
XvFKk01R3BHV+GUKM2RYazpS/P8v7eyKhAbKxOdRcFpHLlVwfjyM1VlDQrEZxsMp
|
||||
NTLYXb6Sce1Uov0YtNx5wEowlREH1WOTlwIDAQAB
|
||||
-----END RSA PUBLIC KEY-----''',
|
||||
|
||||
'''-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBCgKCAQEAsQZnSWVZNfClk29RcDTJQ76n8zZaiTGuUsi8sUhW8AS4PSbPKDm+
|
||||
DyJgdHDWdIF3HBzl7DHeFrILuqTs0vfS7Pa2NW8nUBwiaYQmPtwEa4n7bTmBVGsB
|
||||
1700/tz8wQWOLUlL2nMv+BPlDhxq4kmJCyJfgrIrHlX8sGPcPA4Y6Rwo0MSqYn3s
|
||||
g1Pu5gOKlaT9HKmE6wn5Sut6IiBjWozrRQ6n5h2RXNtO7O2qCDqjgB2vBxhV7B+z
|
||||
hRbLbCmW0tYMDsvPpX5M8fsO05svN+lKtCAuz1leFns8piZpptpSCFn7bWxiA9/f
|
||||
x5x17D7pfah3Sy2pA+NDXyzSlGcKdaUmwQIDAQAB
|
||||
-----END RSA PUBLIC KEY-----''',
|
||||
|
||||
'''-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBCgKCAQEAwqjFW0pi4reKGbkc9pK83Eunwj/k0G8ZTioMMPbZmW99GivMibwa
|
||||
xDM9RDWabEMyUtGoQC2ZcDeLWRK3W8jMP6dnEKAlvLkDLfC4fXYHzFO5KHEqF06i
|
||||
qAqBdmI1iBGdQv/OQCBcbXIWCGDY2AsiqLhlGQfPOI7/vvKc188rTriocgUtoTUc
|
||||
/n/sIUzkgwTqRyvWYynWARWzQg0I9olLBBC2q5RQJJlnYXZwyTL3y9tdb7zOHkks
|
||||
WV9IMQmZmyZh/N7sMbGWQpt4NMchGpPGeJ2e5gHBjDnlIf2p1yZOYeUYrdbwcS0t
|
||||
UiggS4UeE8TzIuXFQxw7fzEIlmhIaq3FnwIDAQAB
|
||||
-----END RSA PUBLIC KEY-----''',
|
||||
):
|
||||
add_key(pub, old=True)
|
|
@ -1 +0,0 @@
|
|||
from .tl.custom import *
|
|
@ -1,46 +0,0 @@
|
|||
"""
|
||||
This module holds all the base and automatically generated errors that the
|
||||
Telegram API has. See telethon_generator/errors.json for more.
|
||||
"""
|
||||
import re
|
||||
|
||||
from .common import (
|
||||
ReadCancelledError, TypeNotFoundError, InvalidChecksumError,
|
||||
InvalidBufferError, AuthKeyNotFound, SecurityError, CdnFileTamperedError,
|
||||
AlreadyInConversationError, BadMessageError, MultiError
|
||||
)
|
||||
|
||||
# This imports the base errors too, as they're imported there
|
||||
from .rpcbaseerrors import *
|
||||
from .rpcerrorlist import *
|
||||
|
||||
|
||||
def rpc_message_to_error(rpc_error, request):
|
||||
"""
|
||||
Converts a Telegram's RPC Error to a Python error.
|
||||
|
||||
:param rpc_error: the RpcError instance.
|
||||
:param request: the request that caused this error.
|
||||
:return: the RPCError as a Python exception that represents this error.
|
||||
"""
|
||||
# Try to get the error by direct look-up, otherwise regex
|
||||
# Case-insensitive, for things like "timeout" which don't conform.
|
||||
cls = rpc_errors_dict.get(rpc_error.error_message.upper(), None)
|
||||
if cls:
|
||||
return cls(request=request)
|
||||
|
||||
for msg_regex, cls in rpc_errors_re:
|
||||
m = re.match(msg_regex, rpc_error.error_message)
|
||||
if m:
|
||||
capture = int(m.group(1)) if m.groups() else None
|
||||
return cls(request=request, capture=capture)
|
||||
|
||||
# Some errors are negative:
|
||||
# * -500 for "No workers running",
|
||||
# * -503 for "Timeout"
|
||||
#
|
||||
# We treat them as if they were positive, so -500 will be treated
|
||||
# as a `ServerError`, etc.
|
||||
cls = base_errors.get(abs(rpc_error.error_code), RPCError)
|
||||
return cls(request=request, message=rpc_error.error_message,
|
||||
code=rpc_error.error_code)
|
|
@ -1,180 +0,0 @@
|
|||
"""Errors not related to the Telegram API itself"""
|
||||
import struct
|
||||
import textwrap
|
||||
|
||||
from ..tl import TLRequest
|
||||
|
||||
|
||||
class ReadCancelledError(Exception):
|
||||
"""Occurs when a read operation was cancelled."""
|
||||
def __init__(self):
|
||||
super().__init__('The read operation was cancelled.')
|
||||
|
||||
|
||||
class TypeNotFoundError(Exception):
|
||||
"""
|
||||
Occurs when a type is not found, for example,
|
||||
when trying to read a TLObject with an invalid constructor code.
|
||||
"""
|
||||
def __init__(self, invalid_constructor_id, remaining):
|
||||
super().__init__(
|
||||
'Could not find a matching Constructor ID for the TLObject '
|
||||
'that was supposed to be read with ID {:08x}. See the FAQ '
|
||||
'for more details. '
|
||||
'Remaining bytes: {!r}'.format(invalid_constructor_id, remaining))
|
||||
|
||||
self.invalid_constructor_id = invalid_constructor_id
|
||||
self.remaining = remaining
|
||||
|
||||
|
||||
class InvalidChecksumError(Exception):
|
||||
"""
|
||||
Occurs when using the TCP full mode and the checksum of a received
|
||||
packet doesn't match the expected checksum.
|
||||
"""
|
||||
def __init__(self, checksum, valid_checksum):
|
||||
super().__init__(
|
||||
'Invalid checksum ({} when {} was expected). '
|
||||
'This packet should be skipped.'
|
||||
.format(checksum, valid_checksum))
|
||||
|
||||
self.checksum = checksum
|
||||
self.valid_checksum = valid_checksum
|
||||
|
||||
|
||||
class InvalidBufferError(BufferError):
|
||||
"""
|
||||
Occurs when the buffer is invalid, and may contain an HTTP error code.
|
||||
For instance, 404 means "forgotten/broken authorization key", while
|
||||
"""
|
||||
def __init__(self, payload):
|
||||
self.payload = payload
|
||||
if len(payload) == 4:
|
||||
self.code = -struct.unpack('<i', payload)[0]
|
||||
super().__init__(
|
||||
'Invalid response buffer (HTTP code {})'.format(self.code))
|
||||
else:
|
||||
self.code = None
|
||||
super().__init__(
|
||||
'Invalid response buffer (too short {})'.format(self.payload))
|
||||
|
||||
|
||||
class AuthKeyNotFound(Exception):
|
||||
"""
|
||||
The server claims it doesn't know about the authorization key (session
|
||||
file) currently being used. This might be because it either has never
|
||||
seen this authorization key, or it used to know about the authorization
|
||||
key but has forgotten it, either temporarily or permanently (possibly
|
||||
due to server errors).
|
||||
|
||||
If the issue persists, you may need to recreate the session file and login
|
||||
again. This is not done automatically because it is not possible to know
|
||||
if the issue is temporary or permanent.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__(textwrap.dedent(self.__class__.__doc__))
|
||||
|
||||
|
||||
class SecurityError(Exception):
|
||||
"""
|
||||
Generic security error, mostly used when generating a new AuthKey.
|
||||
"""
|
||||
def __init__(self, *args):
|
||||
if not args:
|
||||
args = ['A security check failed.']
|
||||
super().__init__(*args)
|
||||
|
||||
|
||||
class CdnFileTamperedError(SecurityError):
|
||||
"""
|
||||
Occurs when there's a hash mismatch between the decrypted CDN file
|
||||
and its expected hash.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'The CDN file has been altered and its download cancelled.'
|
||||
)
|
||||
|
||||
|
||||
class AlreadyInConversationError(Exception):
|
||||
"""
|
||||
Occurs when another exclusive conversation is opened in the same chat.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'Cannot open exclusive conversation in a '
|
||||
'chat that already has one open conversation'
|
||||
)
|
||||
|
||||
|
||||
class BadMessageError(Exception):
|
||||
"""Occurs when handling a bad_message_notification."""
|
||||
ErrorMessages = {
|
||||
16:
|
||||
'msg_id too low (most likely, client time is wrong it would be '
|
||||
'worthwhile to synchronize it using msg_id notifications and re-send '
|
||||
'the original message with the "correct" msg_id or wrap it in a '
|
||||
'container with a new msg_id if the original message had waited too '
|
||||
'long on the client to be transmitted).',
|
||||
17:
|
||||
'msg_id too high (similar to the previous case, the client time has '
|
||||
'to be synchronized, and the message re-sent with the correct msg_id).',
|
||||
18:
|
||||
'Incorrect two lower order msg_id bits (the server expects client '
|
||||
'message msg_id to be divisible by 4).',
|
||||
19:
|
||||
'Container msg_id is the same as msg_id of a previously received '
|
||||
'message (this must never happen).',
|
||||
20:
|
||||
'Message too old, and it cannot be verified whether the server has '
|
||||
'received a message with this msg_id or not.',
|
||||
32:
|
||||
'msg_seqno too low (the server has already received a message with a '
|
||||
'lower msg_id but with either a higher or an equal and odd seqno).',
|
||||
33:
|
||||
'msg_seqno too high (similarly, there is a message with a higher '
|
||||
'msg_id but with either a lower or an equal and odd seqno).',
|
||||
34:
|
||||
'An even msg_seqno expected (irrelevant message), but odd received.',
|
||||
35:
|
||||
'Odd msg_seqno expected (relevant message), but even received.',
|
||||
48:
|
||||
'Incorrect server salt (in this case, the bad_server_salt response '
|
||||
'is received with the correct salt, and the message is to be re-sent '
|
||||
'with it).',
|
||||
64:
|
||||
'Invalid container.'
|
||||
}
|
||||
|
||||
def __init__(self, request, code):
|
||||
super().__init__(request, self.ErrorMessages.get(
|
||||
code,
|
||||
'Unknown error code (this should not happen): {}.'.format(code)))
|
||||
|
||||
self.code = code
|
||||
|
||||
|
||||
class MultiError(Exception):
|
||||
"""Exception container for multiple `TLRequest`'s."""
|
||||
|
||||
def __new__(cls, exceptions, result, requests):
|
||||
if len(result) != len(exceptions) != len(requests):
|
||||
raise ValueError(
|
||||
'Need result, exception and request for each error')
|
||||
for e, req in zip(exceptions, requests):
|
||||
if not isinstance(e, BaseException) and e is not None:
|
||||
raise TypeError(
|
||||
"Expected an exception object, not '%r'" % e
|
||||
)
|
||||
if not isinstance(req, TLRequest):
|
||||
raise TypeError(
|
||||
"Expected TLRequest object, not '%r'" % req
|
||||
)
|
||||
|
||||
if len(exceptions) == 1:
|
||||
return exceptions[0]
|
||||
self = BaseException.__new__(cls)
|
||||
self.exceptions = list(exceptions)
|
||||
self.results = list(result)
|
||||
self.requests = list(requests)
|
||||
return self
|
|
@ -1,131 +0,0 @@
|
|||
from ..tl import functions
|
||||
|
||||
_NESTS_QUERY = (
|
||||
functions.InvokeAfterMsgRequest,
|
||||
functions.InvokeAfterMsgsRequest,
|
||||
functions.InitConnectionRequest,
|
||||
functions.InvokeWithLayerRequest,
|
||||
functions.InvokeWithoutUpdatesRequest,
|
||||
functions.InvokeWithMessagesRangeRequest,
|
||||
functions.InvokeWithTakeoutRequest,
|
||||
)
|
||||
|
||||
class RPCError(Exception):
|
||||
"""Base class for all Remote Procedure Call errors."""
|
||||
code = None
|
||||
message = None
|
||||
|
||||
def __init__(self, request, message, code=None):
|
||||
super().__init__('RPCError {}: {}{}'.format(
|
||||
code or self.code, message, self._fmt_request(request)))
|
||||
|
||||
self.request = request
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
@staticmethod
|
||||
def _fmt_request(request):
|
||||
n = 0
|
||||
reason = ''
|
||||
while isinstance(request, _NESTS_QUERY):
|
||||
n += 1
|
||||
reason += request.__class__.__name__ + '('
|
||||
request = request.query
|
||||
reason += request.__class__.__name__ + ')' * n
|
||||
|
||||
return ' (caused by {})'.format(reason)
|
||||
|
||||
def __reduce__(self):
|
||||
return type(self), (self.request, self.message, self.code)
|
||||
|
||||
|
||||
class InvalidDCError(RPCError):
|
||||
"""
|
||||
The request must be repeated, but directed to a different data center.
|
||||
"""
|
||||
code = 303
|
||||
message = 'ERROR_SEE_OTHER'
|
||||
|
||||
|
||||
class BadRequestError(RPCError):
|
||||
"""
|
||||
The query contains errors. In the event that a request was created
|
||||
using a form and contains user generated data, the user should be
|
||||
notified that the data must be corrected before the query is repeated.
|
||||
"""
|
||||
code = 400
|
||||
message = 'BAD_REQUEST'
|
||||
|
||||
|
||||
class UnauthorizedError(RPCError):
|
||||
"""
|
||||
There was an unauthorized attempt to use functionality available only
|
||||
to authorized users.
|
||||
"""
|
||||
code = 401
|
||||
message = 'UNAUTHORIZED'
|
||||
|
||||
|
||||
class ForbiddenError(RPCError):
|
||||
"""
|
||||
Privacy violation. For example, an attempt to write a message to
|
||||
someone who has blacklisted the current user.
|
||||
"""
|
||||
code = 403
|
||||
message = 'FORBIDDEN'
|
||||
|
||||
|
||||
class NotFoundError(RPCError):
|
||||
"""
|
||||
An attempt to invoke a non-existent object, such as a method.
|
||||
"""
|
||||
code = 404
|
||||
message = 'NOT_FOUND'
|
||||
|
||||
|
||||
class AuthKeyError(RPCError):
|
||||
"""
|
||||
Errors related to invalid authorization key, like
|
||||
AUTH_KEY_DUPLICATED which can cause the connection to fail.
|
||||
"""
|
||||
code = 406
|
||||
message = 'AUTH_KEY'
|
||||
|
||||
|
||||
class FloodError(RPCError):
|
||||
"""
|
||||
The maximum allowed number of attempts to invoke the given method
|
||||
with the given input parameters has been exceeded. For example, in an
|
||||
attempt to request a large number of text messages (SMS) for the same
|
||||
phone number.
|
||||
"""
|
||||
code = 420
|
||||
message = 'FLOOD'
|
||||
|
||||
|
||||
class ServerError(RPCError):
|
||||
"""
|
||||
An internal server error occurred while a request was being processed
|
||||
for example, there was a disruption while accessing a database or file
|
||||
storage.
|
||||
"""
|
||||
code = 500 # Also witnessed as -500
|
||||
message = 'INTERNAL'
|
||||
|
||||
|
||||
class TimedOutError(RPCError):
|
||||
"""
|
||||
Clicking the inline buttons of bots that never (or take to long to)
|
||||
call ``answerCallbackQuery`` will result in this "special" RPCError.
|
||||
"""
|
||||
code = 503 # Only witnessed as -503
|
||||
message = 'Timeout'
|
||||
|
||||
|
||||
BotTimeout = TimedOutError
|
||||
|
||||
|
||||
base_errors = {x.code: x for x in (
|
||||
InvalidDCError, BadRequestError, UnauthorizedError, ForbiddenError,
|
||||
NotFoundError, AuthKeyError, FloodError, ServerError, TimedOutError
|
||||
)}
|
|
@ -1,140 +0,0 @@
|
|||
from .raw import Raw
|
||||
from .album import Album
|
||||
from .chataction import ChatAction
|
||||
from .messagedeleted import MessageDeleted
|
||||
from .messageedited import MessageEdited
|
||||
from .messageread import MessageRead
|
||||
from .newmessage import NewMessage
|
||||
from .userupdate import UserUpdate
|
||||
from .callbackquery import CallbackQuery
|
||||
from .inlinequery import InlineQuery
|
||||
|
||||
|
||||
_HANDLERS_ATTRIBUTE = '__tl.handlers'
|
||||
|
||||
|
||||
class StopPropagation(Exception):
|
||||
"""
|
||||
If this exception is raised in any of the handlers for a given event,
|
||||
it will stop the execution of all other registered event handlers.
|
||||
It can be seen as the ``StopIteration`` in a for loop but for events.
|
||||
|
||||
Example usage:
|
||||
|
||||
>>> from telethon import TelegramClient, events
|
||||
>>> client = TelegramClient(...)
|
||||
>>>
|
||||
>>> @client.on(events.NewMessage)
|
||||
... async def delete(event):
|
||||
... await event.delete()
|
||||
... # No other event handler will have a chance to handle this event
|
||||
... raise StopPropagation
|
||||
...
|
||||
>>> @client.on(events.NewMessage)
|
||||
... async def _(event):
|
||||
... # Will never be reached, because it is the second handler
|
||||
... pass
|
||||
"""
|
||||
# For some reason Sphinx wants the silly >>> or
|
||||
# it will show warnings and look bad when generated.
|
||||
pass
|
||||
|
||||
|
||||
def register(event=None):
|
||||
"""
|
||||
Decorator method to *register* event handlers. This is the client-less
|
||||
`add_event_handler()
|
||||
<telethon.client.updates.UpdateMethods.add_event_handler>` variant.
|
||||
|
||||
Note that this method only registers callbacks as handlers,
|
||||
and does not attach them to any client. This is useful for
|
||||
external modules that don't have access to the client, but
|
||||
still want to define themselves as a handler. Example:
|
||||
|
||||
>>> from telethon import events
|
||||
>>> @events.register(events.NewMessage)
|
||||
... async def handler(event):
|
||||
... ...
|
||||
...
|
||||
>>> # (somewhere else)
|
||||
...
|
||||
>>> from telethon import TelegramClient
|
||||
>>> client = TelegramClient(...)
|
||||
>>> client.add_event_handler(handler)
|
||||
|
||||
Remember that you can use this as a non-decorator
|
||||
through ``register(event)(callback)``.
|
||||
|
||||
Args:
|
||||
event (`_EventBuilder` | `type`):
|
||||
The event builder class or instance to be used,
|
||||
for instance ``events.NewMessage``.
|
||||
"""
|
||||
if isinstance(event, type):
|
||||
event = event()
|
||||
elif not event:
|
||||
event = Raw()
|
||||
|
||||
def decorator(callback):
|
||||
handlers = getattr(callback, _HANDLERS_ATTRIBUTE, [])
|
||||
handlers.append(event)
|
||||
setattr(callback, _HANDLERS_ATTRIBUTE, handlers)
|
||||
return callback
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def unregister(callback, event=None):
|
||||
"""
|
||||
Inverse operation of `register` (though not a decorator). Client-less
|
||||
`remove_event_handler
|
||||
<telethon.client.updates.UpdateMethods.remove_event_handler>`
|
||||
variant. **Note that this won't remove handlers from the client**,
|
||||
because it simply can't, so you would generally use this before
|
||||
adding the handlers to the client.
|
||||
|
||||
This method is here for symmetry. You will rarely need to
|
||||
unregister events, since you can simply just not add them
|
||||
to any client.
|
||||
|
||||
If no event is given, all events for this callback are removed.
|
||||
Returns how many callbacks were removed.
|
||||
"""
|
||||
found = 0
|
||||
if event and not isinstance(event, type):
|
||||
event = type(event)
|
||||
|
||||
handlers = getattr(callback, _HANDLERS_ATTRIBUTE, [])
|
||||
handlers.append((event, callback))
|
||||
i = len(handlers)
|
||||
while i:
|
||||
i -= 1
|
||||
ev = handlers[i]
|
||||
if not event or isinstance(ev, event):
|
||||
del handlers[i]
|
||||
found += 1
|
||||
|
||||
return found
|
||||
|
||||
|
||||
def is_handler(callback):
|
||||
"""
|
||||
Returns `True` if the given callback is an
|
||||
event handler (i.e. you used `register` on it).
|
||||
"""
|
||||
return hasattr(callback, _HANDLERS_ATTRIBUTE)
|
||||
|
||||
|
||||
def list(callback):
|
||||
"""
|
||||
Returns a list containing the registered event
|
||||
builders inside the specified callback handler.
|
||||
"""
|
||||
return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:]
|
||||
|
||||
|
||||
def _get_handlers(callback):
|
||||
"""
|
||||
Like ``list`` but returns `None` if the callback was never registered.
|
||||
"""
|
||||
return getattr(callback, _HANDLERS_ATTRIBUTE, None)
|
|
@ -1,343 +0,0 @@
|
|||
import asyncio
|
||||
import time
|
||||
import weakref
|
||||
|
||||
from .common import EventBuilder, EventCommon, name_inner_event
|
||||
from .. import utils
|
||||
from ..tl import types
|
||||
from ..tl.custom.sendergetter import SenderGetter
|
||||
|
||||
_IGNORE_MAX_SIZE = 100 # len()
|
||||
_IGNORE_MAX_AGE = 5 # seconds
|
||||
|
||||
# IDs to ignore, and when they were added. If it grows too large, we will
|
||||
# remove old entries. Although it should generally not be bigger than 10,
|
||||
# it may be possible some updates are not processed and thus not removed.
|
||||
_IGNORE_DICT = {}
|
||||
|
||||
|
||||
_HACK_DELAY = 0.5
|
||||
|
||||
|
||||
class AlbumHack:
|
||||
"""
|
||||
When receiving an album from a different data-center, they will come in
|
||||
separate `Updates`, so we need to temporarily remember them for a while
|
||||
and only after produce the event.
|
||||
|
||||
Of course events are not designed for this kind of wizardy, so this is
|
||||
a dirty hack that gets the job done.
|
||||
|
||||
When cleaning up the code base we may want to figure out a better way
|
||||
to do this, or just leave the album problem to the users; the update
|
||||
handling code is bad enough as it is.
|
||||
"""
|
||||
def __init__(self, client, event):
|
||||
# It's probably silly to use a weakref here because this object is
|
||||
# very short-lived but might as well try to do "the right thing".
|
||||
self._client = weakref.ref(client)
|
||||
self._event = event # parent event
|
||||
self._due = client.loop.time() + _HACK_DELAY
|
||||
|
||||
client.loop.create_task(self.deliver_event())
|
||||
|
||||
def extend(self, messages):
|
||||
client = self._client()
|
||||
if client: # weakref may be dead
|
||||
self._event.messages.extend(messages)
|
||||
self._due = client.loop.time() + _HACK_DELAY
|
||||
|
||||
async def deliver_event(self):
|
||||
while True:
|
||||
client = self._client()
|
||||
if client is None:
|
||||
return # weakref is dead, nothing to deliver
|
||||
|
||||
diff = self._due - client.loop.time()
|
||||
if diff <= 0:
|
||||
# We've hit our due time, deliver event. It won't respect
|
||||
# sequential updates but fixing that would just worsen this.
|
||||
await client._dispatch_event(self._event)
|
||||
return
|
||||
|
||||
del client # Clear ref and sleep until our due time
|
||||
await asyncio.sleep(diff)
|
||||
|
||||
|
||||
@name_inner_event
|
||||
class Album(EventBuilder):
|
||||
"""
|
||||
Occurs whenever you receive an album. This event only exists
|
||||
to ease dealing with an unknown amount of messages that belong
|
||||
to the same album.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import events
|
||||
|
||||
@client.on(events.Album)
|
||||
async def handler(event):
|
||||
# Counting how many photos or videos the album has
|
||||
print('Got an album with', len(event), 'items')
|
||||
|
||||
# Forwarding the album as a whole to some chat
|
||||
event.forward_to(chat)
|
||||
|
||||
# Printing the caption
|
||||
print(event.text)
|
||||
|
||||
# Replying to the fifth item in the album
|
||||
await event.messages[4].reply('Cool!')
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, chats=None, *, blacklist_chats=False, func=None):
|
||||
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
|
||||
|
||||
@classmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
# TODO normally we'd only check updates if they come with other updates
|
||||
# but MessageBox is not designed for this so others will always be None.
|
||||
# In essence we always rely on AlbumHack rather than returning early if not others.
|
||||
others = [update]
|
||||
|
||||
if isinstance(update,
|
||||
(types.UpdateNewMessage, types.UpdateNewChannelMessage)):
|
||||
if not isinstance(update.message, types.Message):
|
||||
return # We don't care about MessageService's here
|
||||
|
||||
group = update.message.grouped_id
|
||||
if group is None:
|
||||
return # It must be grouped
|
||||
|
||||
# Check whether we are supposed to skip this update, and
|
||||
# if we do also remove it from the ignore list since we
|
||||
# won't need to check against it again.
|
||||
if _IGNORE_DICT.pop(id(update), None):
|
||||
return
|
||||
|
||||
# Check if the ignore list is too big, and if it is clean it
|
||||
# TODO time could technically go backwards; time is not monotonic
|
||||
now = time.time()
|
||||
if len(_IGNORE_DICT) > _IGNORE_MAX_SIZE:
|
||||
for i in [i for i, t in _IGNORE_DICT.items() if now - t > _IGNORE_MAX_AGE]:
|
||||
del _IGNORE_DICT[i]
|
||||
|
||||
# Add the other updates to the ignore list
|
||||
for u in others:
|
||||
if u is not update:
|
||||
_IGNORE_DICT[id(u)] = now
|
||||
|
||||
# Figure out which updates share the same group and use those
|
||||
return cls.Event([
|
||||
u.message for u in others
|
||||
if (isinstance(u, (types.UpdateNewMessage, types.UpdateNewChannelMessage))
|
||||
and isinstance(u.message, types.Message)
|
||||
and u.message.grouped_id == group)
|
||||
])
|
||||
|
||||
def filter(self, event):
|
||||
# Albums with less than two messages require a few hacks to work.
|
||||
if len(event.messages) > 1:
|
||||
return super().filter(event)
|
||||
|
||||
class Event(EventCommon, SenderGetter):
|
||||
"""
|
||||
Represents the event of a new album.
|
||||
|
||||
Members:
|
||||
messages (Sequence[`Message <telethon.tl.custom.message.Message>`]):
|
||||
The list of messages belonging to the same album.
|
||||
"""
|
||||
def __init__(self, messages):
|
||||
message = messages[0]
|
||||
super().__init__(chat_peer=message.peer_id,
|
||||
msg_id=message.id, broadcast=bool(message.post))
|
||||
SenderGetter.__init__(self, message.sender_id)
|
||||
self.messages = messages
|
||||
|
||||
def _set_client(self, client):
|
||||
super()._set_client(client)
|
||||
self._sender, self._input_sender = utils._get_entity_pair(
|
||||
self.sender_id, self._entities, client._mb_entity_cache)
|
||||
|
||||
for msg in self.messages:
|
||||
msg._finish_init(client, self._entities, None)
|
||||
|
||||
if len(self.messages) == 1:
|
||||
# This will require hacks to be a proper album event
|
||||
hack = client._albums.get(self.grouped_id)
|
||||
if hack is None:
|
||||
client._albums[self.grouped_id] = AlbumHack(client, self)
|
||||
else:
|
||||
hack.extend(self.messages)
|
||||
|
||||
@property
|
||||
def grouped_id(self):
|
||||
"""
|
||||
The shared ``grouped_id`` between all the messages.
|
||||
"""
|
||||
return self.messages[0].grouped_id
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
"""
|
||||
The message text of the first photo with a caption,
|
||||
formatted using the client's default parse mode.
|
||||
"""
|
||||
return next((m.text for m in self.messages if m.text), '')
|
||||
|
||||
@property
|
||||
def raw_text(self):
|
||||
"""
|
||||
The raw message text of the first photo
|
||||
with a caption, ignoring any formatting.
|
||||
"""
|
||||
return next((m.raw_text for m in self.messages if m.raw_text), '')
|
||||
|
||||
@property
|
||||
def is_reply(self):
|
||||
"""
|
||||
`True` if the album is a reply to some other message.
|
||||
|
||||
Remember that you can access the ID of the message
|
||||
this one is replying to through `reply_to_msg_id`,
|
||||
and the `Message` object with `get_reply_message()`.
|
||||
"""
|
||||
# Each individual message in an album all reply to the same message
|
||||
return self.messages[0].is_reply
|
||||
|
||||
@property
|
||||
def forward(self):
|
||||
"""
|
||||
The `Forward <telethon.tl.custom.forward.Forward>`
|
||||
information for the first message in the album if it was forwarded.
|
||||
"""
|
||||
# Each individual message in an album all reply to the same message
|
||||
return self.messages[0].forward
|
||||
|
||||
# endregion Public Properties
|
||||
|
||||
# region Public Methods
|
||||
|
||||
async def get_reply_message(self):
|
||||
"""
|
||||
The `Message <telethon.tl.custom.message.Message>`
|
||||
that this album is replying to, or `None`.
|
||||
|
||||
The result will be cached after its first use.
|
||||
"""
|
||||
return await self.messages[0].get_reply_message()
|
||||
|
||||
async def respond(self, *args, **kwargs):
|
||||
"""
|
||||
Responds to the album (not as a reply). Shorthand for
|
||||
`telethon.client.messages.MessageMethods.send_message`
|
||||
with ``entity`` already set.
|
||||
"""
|
||||
return await self.messages[0].respond(*args, **kwargs)
|
||||
|
||||
async def reply(self, *args, **kwargs):
|
||||
"""
|
||||
Replies to the first photo in the album (as a reply). Shorthand
|
||||
for `telethon.client.messages.MessageMethods.send_message`
|
||||
with both ``entity`` and ``reply_to`` already set.
|
||||
"""
|
||||
return await self.messages[0].reply(*args, **kwargs)
|
||||
|
||||
async def forward_to(self, *args, **kwargs):
|
||||
"""
|
||||
Forwards the entire album. Shorthand for
|
||||
`telethon.client.messages.MessageMethods.forward_messages`
|
||||
with both ``messages`` and ``from_peer`` already set.
|
||||
"""
|
||||
if self._client:
|
||||
kwargs['messages'] = self.messages
|
||||
kwargs['from_peer'] = await self.get_input_chat()
|
||||
return await self._client.forward_messages(*args, **kwargs)
|
||||
|
||||
async def edit(self, *args, **kwargs):
|
||||
"""
|
||||
Edits the first caption or the message, or the first messages'
|
||||
caption if no caption is set, iff it's outgoing. Shorthand for
|
||||
`telethon.client.messages.MessageMethods.edit_message`
|
||||
with both ``entity`` and ``message`` already set.
|
||||
|
||||
Returns `None` if the message was incoming,
|
||||
or the edited `Message` otherwise.
|
||||
|
||||
.. note::
|
||||
|
||||
This is different from `client.edit_message
|
||||
<telethon.client.messages.MessageMethods.edit_message>`
|
||||
and **will respect** the previous state of the message.
|
||||
For example, if the message didn't have a link preview,
|
||||
the edit won't add one by default, and you should force
|
||||
it by setting it to `True` if you want it.
|
||||
|
||||
This is generally the most desired and convenient behaviour,
|
||||
and will work for link previews and message buttons.
|
||||
"""
|
||||
for msg in self.messages:
|
||||
if msg.raw_text:
|
||||
return await msg.edit(*args, **kwargs)
|
||||
|
||||
return await self.messages[0].edit(*args, **kwargs)
|
||||
|
||||
async def delete(self, *args, **kwargs):
|
||||
"""
|
||||
Deletes the entire album. You're responsible for checking whether
|
||||
you have the permission to do so, or to except the error otherwise.
|
||||
Shorthand for
|
||||
`telethon.client.messages.MessageMethods.delete_messages` with
|
||||
``entity`` and ``message_ids`` already set.
|
||||
"""
|
||||
if self._client:
|
||||
return await self._client.delete_messages(
|
||||
await self.get_input_chat(), self.messages,
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
async def mark_read(self):
|
||||
"""
|
||||
Marks the entire album as read. Shorthand for
|
||||
`client.send_read_acknowledge()
|
||||
<telethon.client.messages.MessageMethods.send_read_acknowledge>`
|
||||
with both ``entity`` and ``message`` already set.
|
||||
"""
|
||||
if self._client:
|
||||
await self._client.send_read_acknowledge(
|
||||
await self.get_input_chat(), max_id=self.messages[-1].id)
|
||||
|
||||
async def pin(self, *, notify=False):
|
||||
"""
|
||||
Pins the first photo in the album. Shorthand for
|
||||
`telethon.client.messages.MessageMethods.pin_message`
|
||||
with both ``entity`` and ``message`` already set.
|
||||
"""
|
||||
return await self.messages[0].pin(notify=notify)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Return the amount of messages in the album.
|
||||
|
||||
Equivalent to ``len(self.messages)``.
|
||||
"""
|
||||
return len(self.messages)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Iterate over the messages in the album.
|
||||
|
||||
Equivalent to ``iter(self.messages)``.
|
||||
"""
|
||||
return iter(self.messages)
|
||||
|
||||
def __getitem__(self, n):
|
||||
"""
|
||||
Access the n'th message in the album.
|
||||
|
||||
Equivalent to ``event.messages[n]``.
|
||||
"""
|
||||
return self.messages[n]
|
|
@ -1,344 +0,0 @@
|
|||
import re
|
||||
import struct
|
||||
|
||||
from .common import EventBuilder, EventCommon, name_inner_event
|
||||
from .. import utils
|
||||
from ..tl import types, functions
|
||||
from ..tl.custom.sendergetter import SenderGetter
|
||||
|
||||
|
||||
@name_inner_event
|
||||
class CallbackQuery(EventBuilder):
|
||||
"""
|
||||
Occurs whenever you sign in as a bot and a user
|
||||
clicks one of the inline buttons on your messages.
|
||||
|
||||
Note that the `chats` parameter will **not** work with normal
|
||||
IDs or peers if the clicked inline button comes from a "via bot"
|
||||
message. The `chats` parameter also supports checking against the
|
||||
`chat_instance` which should be used for inline callbacks.
|
||||
|
||||
Args:
|
||||
data (`bytes`, `str`, `callable`, optional):
|
||||
If set, the inline button payload data must match this data.
|
||||
A UTF-8 string can also be given, a regex or a callable. For
|
||||
instance, to check against ``'data_1'`` and ``'data_2'`` you
|
||||
can use ``re.compile(b'data_')``.
|
||||
|
||||
pattern (`bytes`, `str`, `callable`, `Pattern`, optional):
|
||||
If set, only buttons with payload matching this pattern will be handled.
|
||||
You can specify a regex-like string which will be matched
|
||||
against the payload data, a callable function that returns `True`
|
||||
if a the payload data is acceptable, or a compiled regex pattern.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import events, Button
|
||||
|
||||
# Handle all callback queries and check data inside the handler
|
||||
@client.on(events.CallbackQuery)
|
||||
async def handler(event):
|
||||
if event.data == b'yes':
|
||||
await event.answer('Correct answer!')
|
||||
|
||||
# Handle only callback queries with data being b'no'
|
||||
@client.on(events.CallbackQuery(data=b'no'))
|
||||
async def handler(event):
|
||||
# Pop-up message with alert
|
||||
await event.answer('Wrong answer!', alert=True)
|
||||
|
||||
# Send a message with buttons users can click
|
||||
async def main():
|
||||
await client.send_message(user, 'Yes or no?', buttons=[
|
||||
Button.inline('Yes!', b'yes'),
|
||||
Button.inline('Nope', b'no')
|
||||
])
|
||||
"""
|
||||
def __init__(
|
||||
self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None):
|
||||
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
|
||||
|
||||
if data and pattern:
|
||||
raise ValueError("Only pass either data or pattern not both.")
|
||||
|
||||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
if isinstance(pattern, str):
|
||||
pattern = pattern.encode('utf-8')
|
||||
|
||||
match = data if data else pattern
|
||||
|
||||
if isinstance(match, bytes):
|
||||
self.match = data if data else re.compile(pattern).match
|
||||
elif not match or callable(match):
|
||||
self.match = match
|
||||
elif hasattr(match, 'match') and callable(match.match):
|
||||
if not isinstance(getattr(match, 'pattern', b''), bytes):
|
||||
match = re.compile(match.pattern.encode('utf-8'),
|
||||
match.flags & (~re.UNICODE))
|
||||
|
||||
self.match = match.match
|
||||
else:
|
||||
raise TypeError('Invalid data or pattern type given')
|
||||
|
||||
self._no_check = all(x is None for x in (
|
||||
self.chats, self.func, self.match,
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
if isinstance(update, types.UpdateBotCallbackQuery):
|
||||
return cls.Event(update, update.peer, update.msg_id)
|
||||
elif isinstance(update, types.UpdateInlineBotCallbackQuery):
|
||||
# See https://github.com/LonamiWebs/Telethon/pull/1005
|
||||
# The long message ID is actually just msg_id + peer_id
|
||||
mid, pid = struct.unpack('<ii', struct.pack('<q', update.msg_id.id))
|
||||
peer = types.PeerChannel(-pid) if pid < 0 else types.PeerUser(pid)
|
||||
return cls.Event(update, peer, mid)
|
||||
|
||||
def filter(self, event):
|
||||
# We can't call super().filter(...) because it ignores chat_instance
|
||||
if self._no_check:
|
||||
return event
|
||||
|
||||
if self.chats is not None:
|
||||
inside = event.query.chat_instance in self.chats
|
||||
if event.chat_id:
|
||||
inside |= event.chat_id in self.chats
|
||||
|
||||
if inside == self.blacklist_chats:
|
||||
return
|
||||
|
||||
if self.match:
|
||||
if callable(self.match):
|
||||
event.data_match = event.pattern_match = self.match(event.query.data)
|
||||
if not event.data_match:
|
||||
return
|
||||
elif event.query.data != self.match:
|
||||
return
|
||||
|
||||
if self.func:
|
||||
# Return the result of func directly as it may need to be awaited
|
||||
return self.func(event)
|
||||
return True
|
||||
|
||||
class Event(EventCommon, SenderGetter):
|
||||
"""
|
||||
Represents the event of a new callback query.
|
||||
|
||||
Members:
|
||||
query (:tl:`UpdateBotCallbackQuery`):
|
||||
The original :tl:`UpdateBotCallbackQuery`.
|
||||
|
||||
data_match (`obj`, optional):
|
||||
The object returned by the ``data=`` parameter
|
||||
when creating the event builder, if any. Similar
|
||||
to ``pattern_match`` for the new message event.
|
||||
|
||||
pattern_match (`obj`, optional):
|
||||
Alias for ``data_match``.
|
||||
"""
|
||||
def __init__(self, query, peer, msg_id):
|
||||
super().__init__(peer, msg_id=msg_id)
|
||||
SenderGetter.__init__(self, query.user_id)
|
||||
self.query = query
|
||||
self.data_match = None
|
||||
self.pattern_match = None
|
||||
self._message = None
|
||||
self._answered = False
|
||||
|
||||
def _set_client(self, client):
|
||||
super()._set_client(client)
|
||||
self._sender, self._input_sender = utils._get_entity_pair(
|
||||
self.sender_id, self._entities, client._mb_entity_cache)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""
|
||||
Returns the query ID. The user clicking the inline
|
||||
button is the one who generated this random ID.
|
||||
"""
|
||||
return self.query.query_id
|
||||
|
||||
@property
|
||||
def message_id(self):
|
||||
"""
|
||||
Returns the message ID to which the clicked inline button belongs.
|
||||
"""
|
||||
return self._message_id
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""
|
||||
Returns the data payload from the original inline button.
|
||||
"""
|
||||
return self.query.data
|
||||
|
||||
@property
|
||||
def chat_instance(self):
|
||||
"""
|
||||
Unique identifier for the chat where the callback occurred.
|
||||
Useful for high scores in games.
|
||||
"""
|
||||
return self.query.chat_instance
|
||||
|
||||
async def get_message(self):
|
||||
"""
|
||||
Returns the message to which the clicked inline button belongs.
|
||||
"""
|
||||
if self._message is not None:
|
||||
return self._message
|
||||
|
||||
try:
|
||||
chat = await self.get_input_chat() if self.is_channel else None
|
||||
self._message = await self._client.get_messages(
|
||||
chat, ids=self._message_id)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
return self._message
|
||||
|
||||
async def _refetch_sender(self):
|
||||
self._sender = self._entities.get(self.sender_id)
|
||||
if not self._sender:
|
||||
return
|
||||
|
||||
self._input_sender = utils.get_input_peer(self._chat)
|
||||
if not getattr(self._input_sender, 'access_hash', True):
|
||||
# getattr with True to handle the InputPeerSelf() case
|
||||
try:
|
||||
self._input_sender = self._client._mb_entity_cache.get(
|
||||
utils.resolve_id(self._sender_id)[0])._as_input_peer()
|
||||
except AttributeError:
|
||||
m = await self.get_message()
|
||||
if m:
|
||||
self._sender = m._sender
|
||||
self._input_sender = m._input_sender
|
||||
|
||||
async def answer(
|
||||
self, message=None, cache_time=0, *, url=None, alert=False):
|
||||
"""
|
||||
Answers the callback query (and stops the loading circle).
|
||||
|
||||
Args:
|
||||
message (`str`, optional):
|
||||
The toast message to show feedback to the user.
|
||||
|
||||
cache_time (`int`, optional):
|
||||
For how long this result should be cached on
|
||||
the user's client. Defaults to 0 for no cache.
|
||||
|
||||
url (`str`, optional):
|
||||
The URL to be opened in the user's client. Note that
|
||||
the only valid URLs are those of games your bot has,
|
||||
or alternatively a 't.me/your_bot?start=xyz' parameter.
|
||||
|
||||
alert (`bool`, optional):
|
||||
Whether an alert (a pop-up dialog) should be used
|
||||
instead of showing a toast. Defaults to `False`.
|
||||
"""
|
||||
if self._answered:
|
||||
return
|
||||
|
||||
self._answered = True
|
||||
return await self._client(
|
||||
functions.messages.SetBotCallbackAnswerRequest(
|
||||
query_id=self.query.query_id,
|
||||
cache_time=cache_time,
|
||||
alert=alert,
|
||||
message=message,
|
||||
url=url
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def via_inline(self):
|
||||
"""
|
||||
Whether this callback was generated from an inline button sent
|
||||
via an inline query or not. If the bot sent the message itself
|
||||
with buttons, and one of those is clicked, this will be `False`.
|
||||
If a user sent the message coming from an inline query to the
|
||||
bot, and one of those is clicked, this will be `True`.
|
||||
|
||||
If it's `True`, it's likely that the bot is **not** in the
|
||||
chat, so methods like `respond` or `delete` won't work (but
|
||||
`edit` will always work).
|
||||
"""
|
||||
return isinstance(self.query, types.UpdateInlineBotCallbackQuery)
|
||||
|
||||
async def respond(self, *args, **kwargs):
|
||||
"""
|
||||
Responds to the message (not as a reply). Shorthand for
|
||||
`telethon.client.messages.MessageMethods.send_message` with
|
||||
``entity`` already set.
|
||||
|
||||
This method also creates a task to `answer` the callback.
|
||||
|
||||
This method will likely fail if `via_inline` is `True`.
|
||||
"""
|
||||
self._client.loop.create_task(self.answer())
|
||||
return await self._client.send_message(
|
||||
await self.get_input_chat(), *args, **kwargs)
|
||||
|
||||
async def reply(self, *args, **kwargs):
|
||||
"""
|
||||
Replies to the message (as a reply). Shorthand for
|
||||
`telethon.client.messages.MessageMethods.send_message` with
|
||||
both ``entity`` and ``reply_to`` already set.
|
||||
|
||||
This method also creates a task to `answer` the callback.
|
||||
|
||||
This method will likely fail if `via_inline` is `True`.
|
||||
"""
|
||||
self._client.loop.create_task(self.answer())
|
||||
kwargs['reply_to'] = self.query.msg_id
|
||||
return await self._client.send_message(
|
||||
await self.get_input_chat(), *args, **kwargs)
|
||||
|
||||
async def edit(self, *args, **kwargs):
|
||||
"""
|
||||
Edits the message. Shorthand for
|
||||
`telethon.client.messages.MessageMethods.edit_message` with
|
||||
the ``entity`` set to the correct :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64`.
|
||||
|
||||
Returns `True` if the edit was successful.
|
||||
|
||||
This method also creates a task to `answer` the callback.
|
||||
|
||||
.. note::
|
||||
|
||||
This method won't respect the previous message unlike
|
||||
`Message.edit <telethon.tl.custom.message.Message.edit>`,
|
||||
since the message object is normally not present.
|
||||
"""
|
||||
self._client.loop.create_task(self.answer())
|
||||
if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
|
||||
return await self._client.edit_message(
|
||||
self.query.msg_id, *args, **kwargs
|
||||
)
|
||||
else:
|
||||
return await self._client.edit_message(
|
||||
await self.get_input_chat(), self.query.msg_id,
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
async def delete(self, *args, **kwargs):
|
||||
"""
|
||||
Deletes the message. Shorthand for
|
||||
`telethon.client.messages.MessageMethods.delete_messages` with
|
||||
``entity`` and ``message_ids`` already set.
|
||||
|
||||
If you need to delete more than one message at once, don't use
|
||||
this `delete` method. Use a
|
||||
`telethon.client.telegramclient.TelegramClient` instance directly.
|
||||
|
||||
This method also creates a task to `answer` the callback.
|
||||
|
||||
This method will likely fail if `via_inline` is `True`.
|
||||
"""
|
||||
self._client.loop.create_task(self.answer())
|
||||
return await self._client.delete_messages(
|
||||
await self.get_input_chat(), [self.query.msg_id],
|
||||
*args, **kwargs
|
||||
)
|
|
@ -1,458 +0,0 @@
|
|||
from .common import EventBuilder, EventCommon, name_inner_event
|
||||
from .. import utils
|
||||
from ..tl import types
|
||||
|
||||
|
||||
@name_inner_event
|
||||
class ChatAction(EventBuilder):
|
||||
"""
|
||||
Occurs on certain chat actions:
|
||||
|
||||
* Whenever a new chat is created.
|
||||
* Whenever a chat's title or photo is changed or removed.
|
||||
* Whenever a new message is pinned.
|
||||
* Whenever a user scores in a game.
|
||||
* Whenever a user joins or is added to the group.
|
||||
* Whenever a user is removed or leaves a group if it has
|
||||
less than 50 members or the removed user was a bot.
|
||||
|
||||
Note that "chat" refers to "small group, megagroup and broadcast
|
||||
channel", whereas "group" refers to "small group and megagroup" only.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import events
|
||||
|
||||
@client.on(events.ChatAction)
|
||||
async def handler(event):
|
||||
# Welcome every new user
|
||||
if event.user_joined:
|
||||
await event.reply('Welcome to the group!')
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
# Rely on specific pin updates for unpins, but otherwise ignore them
|
||||
# for new pins (we'd rather handle the new service message with pin,
|
||||
# so that we can act on that message').
|
||||
if isinstance(update, types.UpdatePinnedChannelMessages) and not update.pinned:
|
||||
return cls.Event(types.PeerChannel(update.channel_id),
|
||||
pin_ids=update.messages,
|
||||
pin=update.pinned)
|
||||
|
||||
elif isinstance(update, types.UpdatePinnedMessages) and not update.pinned:
|
||||
return cls.Event(update.peer,
|
||||
pin_ids=update.messages,
|
||||
pin=update.pinned)
|
||||
|
||||
elif isinstance(update, types.UpdateChatParticipantAdd):
|
||||
return cls.Event(types.PeerChat(update.chat_id),
|
||||
added_by=update.inviter_id or True,
|
||||
users=update.user_id)
|
||||
|
||||
elif isinstance(update, types.UpdateChatParticipantDelete):
|
||||
return cls.Event(types.PeerChat(update.chat_id),
|
||||
kicked_by=True,
|
||||
users=update.user_id)
|
||||
|
||||
# UpdateChannel is sent if we leave a channel, and the update._entities
|
||||
# set by _process_update would let us make some guesses. However it's
|
||||
# better not to rely on this. Rely only in MessageActionChatDeleteUser.
|
||||
|
||||
elif (isinstance(update, (
|
||||
types.UpdateNewMessage, types.UpdateNewChannelMessage))
|
||||
and isinstance(update.message, types.MessageService)):
|
||||
msg = update.message
|
||||
action = update.message.action
|
||||
if isinstance(action, types.MessageActionChatJoinedByLink):
|
||||
return cls.Event(msg,
|
||||
added_by=True,
|
||||
users=msg.from_id)
|
||||
elif isinstance(action, types.MessageActionChatAddUser):
|
||||
# If a user adds itself, it means they joined via the public chat username
|
||||
added_by = ([msg.sender_id] == action.users) or msg.from_id
|
||||
return cls.Event(msg,
|
||||
added_by=added_by,
|
||||
users=action.users)
|
||||
elif isinstance(action, types.MessageActionChatDeleteUser):
|
||||
return cls.Event(msg,
|
||||
kicked_by=utils.get_peer_id(msg.from_id) if msg.from_id else True,
|
||||
users=action.user_id)
|
||||
elif isinstance(action, types.MessageActionChatCreate):
|
||||
return cls.Event(msg,
|
||||
users=action.users,
|
||||
created=True,
|
||||
new_title=action.title)
|
||||
elif isinstance(action, types.MessageActionChannelCreate):
|
||||
return cls.Event(msg,
|
||||
created=True,
|
||||
users=msg.from_id,
|
||||
new_title=action.title)
|
||||
elif isinstance(action, types.MessageActionChatEditTitle):
|
||||
return cls.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_title=action.title)
|
||||
elif isinstance(action, types.MessageActionChatEditPhoto):
|
||||
return cls.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_photo=action.photo)
|
||||
elif isinstance(action, types.MessageActionChatDeletePhoto):
|
||||
return cls.Event(msg,
|
||||
users=msg.from_id,
|
||||
new_photo=True)
|
||||
elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to:
|
||||
return cls.Event(msg,
|
||||
pin_ids=[msg.reply_to_msg_id])
|
||||
elif isinstance(action, types.MessageActionGameScore):
|
||||
return cls.Event(msg,
|
||||
new_score=action.score)
|
||||
|
||||
elif isinstance(update, types.UpdateChannelParticipant) \
|
||||
and bool(update.new_participant) != bool(update.prev_participant):
|
||||
# If members are hidden, bots will receive this update instead,
|
||||
# as there won't be a service message. Promotions and demotions
|
||||
# seem to have both new and prev participant, which are ignored
|
||||
# by this event.
|
||||
return cls.Event(types.PeerChannel(update.channel_id),
|
||||
users=update.user_id,
|
||||
added_by=update.actor_id if update.new_participant else None,
|
||||
kicked_by=update.actor_id if update.prev_participant else None)
|
||||
|
||||
class Event(EventCommon):
|
||||
"""
|
||||
Represents the event of a new chat action.
|
||||
|
||||
Members:
|
||||
action_message (`MessageAction <https://tl.telethon.dev/types/message_action.html>`_):
|
||||
The message invoked by this Chat Action.
|
||||
|
||||
new_pin (`bool`):
|
||||
`True` if there is a new pin.
|
||||
|
||||
new_photo (`bool`):
|
||||
`True` if there's a new chat photo (or it was removed).
|
||||
|
||||
photo (:tl:`Photo`, optional):
|
||||
The new photo (or `None` if it was removed).
|
||||
|
||||
user_added (`bool`):
|
||||
`True` if the user was added by some other.
|
||||
|
||||
user_joined (`bool`):
|
||||
`True` if the user joined on their own.
|
||||
|
||||
user_left (`bool`):
|
||||
`True` if the user left on their own.
|
||||
|
||||
user_kicked (`bool`):
|
||||
`True` if the user was kicked by some other.
|
||||
|
||||
created (`bool`, optional):
|
||||
`True` if this chat was just created.
|
||||
|
||||
new_title (`str`, optional):
|
||||
The new title string for the chat, if applicable.
|
||||
|
||||
new_score (`str`, optional):
|
||||
The new score string for the game, if applicable.
|
||||
|
||||
unpin (`bool`):
|
||||
`True` if the existing pin gets unpinned.
|
||||
"""
|
||||
|
||||
def __init__(self, where, new_photo=None,
|
||||
added_by=None, kicked_by=None, created=None,
|
||||
users=None, new_title=None, pin_ids=None, pin=None, new_score=None):
|
||||
if isinstance(where, types.MessageService):
|
||||
self.action_message = where
|
||||
where = where.peer_id
|
||||
else:
|
||||
self.action_message = None
|
||||
|
||||
# TODO needs some testing (can there be more than one id, and do they follow pin order?)
|
||||
# same in get_pinned_message
|
||||
super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None)
|
||||
|
||||
self.new_pin = pin_ids is not None
|
||||
self._pin_ids = pin_ids
|
||||
self._pinned_messages = None
|
||||
|
||||
self.new_photo = new_photo is not None
|
||||
self.photo = \
|
||||
new_photo if isinstance(new_photo, types.Photo) else None
|
||||
|
||||
self._added_by = None
|
||||
self._kicked_by = None
|
||||
self.user_added = self.user_joined = self.user_left = \
|
||||
self.user_kicked = self.unpin = False
|
||||
|
||||
if added_by is True:
|
||||
self.user_joined = True
|
||||
elif added_by:
|
||||
self.user_added = True
|
||||
self._added_by = added_by
|
||||
|
||||
# If `from_id` was not present (it's `True`) or the affected
|
||||
# user was "kicked by itself", then it left. Else it was kicked.
|
||||
if kicked_by is True or (users is not None and kicked_by == users):
|
||||
self.user_left = True
|
||||
elif kicked_by:
|
||||
self.user_kicked = True
|
||||
self._kicked_by = kicked_by
|
||||
|
||||
self.created = bool(created)
|
||||
|
||||
if isinstance(users, list):
|
||||
self._user_ids = [utils.get_peer_id(u) for u in users]
|
||||
elif users:
|
||||
self._user_ids = [utils.get_peer_id(users)]
|
||||
else:
|
||||
self._user_ids = []
|
||||
|
||||
self._users = None
|
||||
self._input_users = None
|
||||
self.new_title = new_title
|
||||
self.new_score = new_score
|
||||
self.unpin = not pin
|
||||
|
||||
def _set_client(self, client):
|
||||
super()._set_client(client)
|
||||
if self.action_message:
|
||||
self.action_message._finish_init(client, self._entities, None)
|
||||
|
||||
async def respond(self, *args, **kwargs):
|
||||
"""
|
||||
Responds to the chat action message (not as a reply). Shorthand for
|
||||
`telethon.client.messages.MessageMethods.send_message` with
|
||||
``entity`` already set.
|
||||
"""
|
||||
return await self._client.send_message(
|
||||
await self.get_input_chat(), *args, **kwargs)
|
||||
|
||||
async def reply(self, *args, **kwargs):
|
||||
"""
|
||||
Replies to the chat action message (as a reply). Shorthand for
|
||||
`telethon.client.messages.MessageMethods.send_message` with
|
||||
both ``entity`` and ``reply_to`` already set.
|
||||
|
||||
Has the same effect as `respond` if there is no message.
|
||||
"""
|
||||
if not self.action_message:
|
||||
return await self.respond(*args, **kwargs)
|
||||
|
||||
kwargs['reply_to'] = self.action_message.id
|
||||
return await self._client.send_message(
|
||||
await self.get_input_chat(), *args, **kwargs)
|
||||
|
||||
async 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
|
||||
otherwise. Shorthand for
|
||||
`telethon.client.messages.MessageMethods.delete_messages` with
|
||||
``entity`` and ``message_ids`` already set.
|
||||
|
||||
Does nothing if no message action triggered this event.
|
||||
"""
|
||||
if not self.action_message:
|
||||
return
|
||||
|
||||
return await self._client.delete_messages(
|
||||
await self.get_input_chat(), [self.action_message],
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
async def get_pinned_message(self):
|
||||
"""
|
||||
If ``new_pin`` is `True`, this returns the `Message
|
||||
<telethon.tl.custom.message.Message>` object that was pinned.
|
||||
"""
|
||||
if self._pinned_messages is None:
|
||||
await self.get_pinned_messages()
|
||||
|
||||
if self._pinned_messages:
|
||||
return self._pinned_messages[0]
|
||||
|
||||
async def get_pinned_messages(self):
|
||||
"""
|
||||
If ``new_pin`` is `True`, this returns a `list` of `Message
|
||||
<telethon.tl.custom.message.Message>` objects that were pinned.
|
||||
"""
|
||||
if not self._pin_ids:
|
||||
return self._pin_ids # either None or empty list
|
||||
|
||||
chat = await self.get_input_chat()
|
||||
if chat:
|
||||
self._pinned_messages = await self._client.get_messages(
|
||||
self._input_chat, ids=self._pin_ids)
|
||||
|
||||
return self._pinned_messages
|
||||
|
||||
@property
|
||||
def added_by(self):
|
||||
"""
|
||||
The user who added ``users``, if applicable (`None` otherwise).
|
||||
"""
|
||||
if self._added_by and not isinstance(self._added_by, types.User):
|
||||
aby = self._entities.get(utils.get_peer_id(self._added_by))
|
||||
if aby:
|
||||
self._added_by = aby
|
||||
|
||||
return self._added_by
|
||||
|
||||
async 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)
|
||||
|
||||
return self._added_by
|
||||
|
||||
@property
|
||||
def kicked_by(self):
|
||||
"""
|
||||
The user who kicked ``users``, if applicable (`None` otherwise).
|
||||
"""
|
||||
if self._kicked_by and not isinstance(self._kicked_by, types.User):
|
||||
kby = self._entities.get(utils.get_peer_id(self._kicked_by))
|
||||
if kby:
|
||||
self._kicked_by = kby
|
||||
|
||||
return self._kicked_by
|
||||
|
||||
async 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)
|
||||
|
||||
return self._kicked_by
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
"""
|
||||
The first user that takes part in this action. For example, who joined.
|
||||
|
||||
Might be `None` if the information can't be retrieved or
|
||||
there is no user taking part.
|
||||
"""
|
||||
if self.users:
|
||||
return self._users[0]
|
||||
|
||||
async def get_user(self):
|
||||
"""
|
||||
Returns `user` but will make an API call if necessary.
|
||||
"""
|
||||
if self.users or await self.get_users():
|
||||
return self._users[0]
|
||||
|
||||
@property
|
||||
def input_user(self):
|
||||
"""
|
||||
Input version of the ``self.user`` property.
|
||||
"""
|
||||
if self.input_users:
|
||||
return self._input_users[0]
|
||||
|
||||
async 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():
|
||||
return self._input_users[0]
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
"""
|
||||
Returns the marked signed ID of the first user, if any.
|
||||
"""
|
||||
if self._user_ids:
|
||||
return self._user_ids[0]
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
"""
|
||||
A list of users that take part in this action. For example, who joined.
|
||||
|
||||
Might be empty if the information can't be retrieved or there
|
||||
are no users taking part.
|
||||
"""
|
||||
if not self._user_ids:
|
||||
return []
|
||||
|
||||
if self._users is None:
|
||||
self._users = [
|
||||
self._entities[user_id]
|
||||
for user_id in self._user_ids
|
||||
if user_id in self._entities
|
||||
]
|
||||
|
||||
return self._users
|
||||
|
||||
async def get_users(self):
|
||||
"""
|
||||
Returns `users` but will make an API call if necessary.
|
||||
"""
|
||||
if not self._user_ids:
|
||||
return []
|
||||
|
||||
# Note: we access the property first so that it fills if needed
|
||||
if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message:
|
||||
await self.action_message._reload_message()
|
||||
self._users = [
|
||||
u for u in self.action_message.action_entities
|
||||
if isinstance(u, (types.User, types.UserEmpty))]
|
||||
|
||||
return self._users
|
||||
|
||||
@property
|
||||
def input_users(self):
|
||||
"""
|
||||
Input version of the ``self.users`` property.
|
||||
"""
|
||||
if self._input_users is None and self._user_ids:
|
||||
self._input_users = []
|
||||
for user_id in self._user_ids:
|
||||
# First try to get it from our entities
|
||||
try:
|
||||
self._input_users.append(utils.get_input_peer(self._entities[user_id]))
|
||||
continue
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
# If missing, try from the entity cache
|
||||
try:
|
||||
self._input_users.append(self._client._mb_entity_cache.get(
|
||||
utils.resolve_id(user_id)[0])._as_input_peer())
|
||||
continue
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return self._input_users or []
|
||||
|
||||
async def get_input_users(self):
|
||||
"""
|
||||
Returns `input_users` but will make an API call if necessary.
|
||||
"""
|
||||
if not self._user_ids:
|
||||
return []
|
||||
|
||||
# Note: we access the property first so that it fills if needed
|
||||
if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message:
|
||||
self._input_users = [
|
||||
utils.get_input_peer(u)
|
||||
for u in self.action_message.action_entities
|
||||
if isinstance(u, (types.User, types.UserEmpty))]
|
||||
|
||||
return self._input_users or []
|
||||
|
||||
@property
|
||||
def user_ids(self):
|
||||
"""
|
||||
Returns the marked signed ID of the users, if any.
|
||||
"""
|
||||
if self._user_ids:
|
||||
return self._user_ids[:]
|
|
@ -1,186 +0,0 @@
|
|||
import abc
|
||||
import asyncio
|
||||
import warnings
|
||||
|
||||
from .. import utils
|
||||
from ..tl import TLObject, types
|
||||
from ..tl.custom.chatgetter import ChatGetter
|
||||
|
||||
|
||||
async 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
|
||||
|
||||
if not utils.is_list_like(chats):
|
||||
chats = (chats,)
|
||||
|
||||
result = set()
|
||||
for chat in chats:
|
||||
if isinstance(chat, int):
|
||||
if chat < 0:
|
||||
result.add(chat) # Explicitly marked IDs are negative
|
||||
else:
|
||||
result.update({ # Support all valid types of peers
|
||||
utils.get_peer_id(types.PeerUser(chat)),
|
||||
utils.get_peer_id(types.PeerChat(chat)),
|
||||
utils.get_peer_id(types.PeerChannel(chat)),
|
||||
})
|
||||
elif isinstance(chat, TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687:
|
||||
# 0x2d45687 == crc32(b'Peer')
|
||||
result.add(utils.get_peer_id(chat))
|
||||
else:
|
||||
chat = await client.get_input_entity(chat)
|
||||
if isinstance(chat, types.InputPeerSelf):
|
||||
chat = await client.get_me(input_peer=True)
|
||||
result.add(utils.get_peer_id(chat))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class EventBuilder(abc.ABC):
|
||||
"""
|
||||
The common event builder, with builtin support to filter per chat.
|
||||
|
||||
Args:
|
||||
chats (`entity`, optional):
|
||||
May be one or more entities (username/peer/etc.), preferably IDs.
|
||||
By default, only matching chats will be handled.
|
||||
|
||||
blacklist_chats (`bool`, optional):
|
||||
Whether to treat the chats as a blacklist instead of
|
||||
as a whitelist (default). This means that every chat
|
||||
will be handled *except* those specified in ``chats``
|
||||
which will be ignored if ``blacklist_chats=True``.
|
||||
|
||||
func (`callable`, optional):
|
||||
A callable (async or not) function that should accept the event as input
|
||||
parameter, and return a value indicating whether the event
|
||||
should be dispatched or not (any truthy value will do, it
|
||||
does not need to be a `bool`). It works like a custom filter:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@client.on(events.NewMessage(func=lambda e: e.is_private))
|
||||
async def handler(event):
|
||||
pass # code here
|
||||
"""
|
||||
def __init__(self, chats=None, *, blacklist_chats=False, func=None):
|
||||
self.chats = chats
|
||||
self.blacklist_chats = bool(blacklist_chats)
|
||||
self.resolved = False
|
||||
self.func = func
|
||||
self._resolve_lock = None
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
"""
|
||||
Builds an event for the given update if possible, or returns None.
|
||||
|
||||
`others` are the rest of updates that came in the same container
|
||||
as the current `update`.
|
||||
|
||||
`self_id` should be the current user's ID, since it is required
|
||||
for some events which lack this information but still need it.
|
||||
"""
|
||||
# TODO So many parameters specific to only some update types seems dirty
|
||||
|
||||
async def resolve(self, client):
|
||||
"""Helper method to allow event builders to be resolved before usage"""
|
||||
if self.resolved:
|
||||
return
|
||||
|
||||
if not self._resolve_lock:
|
||||
self._resolve_lock = asyncio.Lock()
|
||||
|
||||
async with self._resolve_lock:
|
||||
if not self.resolved:
|
||||
await self._resolve(client)
|
||||
self.resolved = True
|
||||
|
||||
async def _resolve(self, client):
|
||||
self.chats = await _into_id_set(client, self.chats)
|
||||
|
||||
def filter(self, event):
|
||||
"""
|
||||
Returns a truthy value if the event passed the filter and should be
|
||||
used, or falsy otherwise. The return value may need to be awaited.
|
||||
|
||||
The events must have been resolved before this can be called.
|
||||
"""
|
||||
if not self.resolved:
|
||||
return
|
||||
|
||||
if self.chats is not None:
|
||||
# Note: the `event.chat_id` property checks if it's `None` for us
|
||||
inside = event.chat_id in self.chats
|
||||
if inside == self.blacklist_chats:
|
||||
# If this chat matches but it's a blacklist ignore.
|
||||
# If it doesn't match but it's a whitelist ignore.
|
||||
return
|
||||
|
||||
if not self.func:
|
||||
return True
|
||||
|
||||
# Return the result of func directly as it may need to be awaited
|
||||
return self.func(event)
|
||||
|
||||
|
||||
class EventCommon(ChatGetter, abc.ABC):
|
||||
"""
|
||||
Intermediate class with common things to all events.
|
||||
|
||||
Remember that this class implements `ChatGetter
|
||||
<telethon.tl.custom.chatgetter.ChatGetter>` which
|
||||
means you have access to all chat properties and methods.
|
||||
|
||||
In addition, you can access the `original_update`
|
||||
field which contains the original :tl:`Update`.
|
||||
"""
|
||||
_event_name = 'Event'
|
||||
|
||||
def __init__(self, chat_peer=None, msg_id=None, broadcast=None):
|
||||
super().__init__(chat_peer, broadcast=broadcast)
|
||||
self._entities = {}
|
||||
self._client = None
|
||||
self._message_id = msg_id
|
||||
self.original_update = None
|
||||
|
||||
def _set_client(self, client):
|
||||
"""
|
||||
Setter so subclasses can act accordingly when the client is set.
|
||||
"""
|
||||
self._client = client
|
||||
if self._chat_peer:
|
||||
self._chat, self._input_chat = utils._get_entity_pair(
|
||||
self.chat_id, self._entities, client._mb_entity_cache)
|
||||
else:
|
||||
self._chat = self._input_chat = None
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
"""
|
||||
The `telethon.TelegramClient` that created this event.
|
||||
"""
|
||||
return self._client
|
||||
|
||||
def __str__(self):
|
||||
return TLObject.pretty_format(self.to_dict())
|
||||
|
||||
def stringify(self):
|
||||
return TLObject.pretty_format(self.to_dict(), indent=0)
|
||||
|
||||
def to_dict(self):
|
||||
d = {k: v for k, v in self.__dict__.items() if k[0] != '_'}
|
||||
d['_'] = self._event_name
|
||||
return d
|
||||
|
||||
|
||||
def name_inner_event(cls):
|
||||
"""Decorator to rename cls.Event 'Event' as 'cls.Event'"""
|
||||
if hasattr(cls, 'Event'):
|
||||
cls.Event._event_name = '{}.Event'.format(cls.__name__)
|
||||
else:
|
||||
warnings.warn('Class {} does not have a inner Event'.format(cls))
|
||||
return cls
|
|
@ -1,247 +0,0 @@
|
|||
import inspect
|
||||
import re
|
||||
|
||||
import asyncio
|
||||
|
||||
from .common import EventBuilder, EventCommon, name_inner_event
|
||||
from .. import utils, helpers
|
||||
from ..tl import types, functions, custom
|
||||
from ..tl.custom.sendergetter import SenderGetter
|
||||
|
||||
|
||||
@name_inner_event
|
||||
class InlineQuery(EventBuilder):
|
||||
"""
|
||||
Occurs whenever you sign in as a bot and a user
|
||||
sends an inline query such as ``@bot query``.
|
||||
|
||||
Args:
|
||||
users (`entity`, optional):
|
||||
May be one or more entities (username/peer/etc.), preferably IDs.
|
||||
By default, only inline queries from these users will be handled.
|
||||
|
||||
blacklist_users (`bool`, optional):
|
||||
Whether to treat the users as a blacklist instead of
|
||||
as a whitelist (default). This means that every chat
|
||||
will be handled *except* those specified in ``users``
|
||||
which will be ignored if ``blacklist_users=True``.
|
||||
|
||||
pattern (`str`, `callable`, `Pattern`, optional):
|
||||
If set, only queries matching this pattern will be handled.
|
||||
You can specify a regex-like string which will be matched
|
||||
against the message, a callable function that returns `True`
|
||||
if a message is acceptable, or a compiled regex pattern.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import events
|
||||
|
||||
@client.on(events.InlineQuery)
|
||||
async def handler(event):
|
||||
builder = event.builder
|
||||
|
||||
# Two options (convert user text to UPPERCASE or lowercase)
|
||||
await event.answer([
|
||||
builder.article('UPPERCASE', text=event.text.upper()),
|
||||
builder.article('lowercase', text=event.text.lower()),
|
||||
])
|
||||
"""
|
||||
def __init__(
|
||||
self, users=None, *, blacklist_users=False, func=None, pattern=None):
|
||||
super().__init__(users, blacklist_chats=blacklist_users, func=func)
|
||||
|
||||
if isinstance(pattern, str):
|
||||
self.pattern = re.compile(pattern).match
|
||||
elif not pattern or callable(pattern):
|
||||
self.pattern = pattern
|
||||
elif hasattr(pattern, 'match') and callable(pattern.match):
|
||||
self.pattern = pattern.match
|
||||
else:
|
||||
raise TypeError('Invalid pattern type given')
|
||||
|
||||
@classmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
if isinstance(update, types.UpdateBotInlineQuery):
|
||||
return cls.Event(update)
|
||||
|
||||
def filter(self, event):
|
||||
if self.pattern:
|
||||
match = self.pattern(event.text)
|
||||
if not match:
|
||||
return
|
||||
event.pattern_match = match
|
||||
|
||||
return super().filter(event)
|
||||
|
||||
class Event(EventCommon, SenderGetter):
|
||||
"""
|
||||
Represents the event of a new callback query.
|
||||
|
||||
Members:
|
||||
query (:tl:`UpdateBotInlineQuery`):
|
||||
The original :tl:`UpdateBotInlineQuery`.
|
||||
|
||||
Make sure to access the `text` property of the query if
|
||||
you want the text rather than the actual query object.
|
||||
|
||||
pattern_match (`obj`, optional):
|
||||
The resulting object from calling the passed ``pattern``
|
||||
function, which is ``re.compile(...).match`` by default.
|
||||
"""
|
||||
def __init__(self, query):
|
||||
super().__init__(chat_peer=types.PeerUser(query.user_id))
|
||||
SenderGetter.__init__(self, query.user_id)
|
||||
self.query = query
|
||||
self.pattern_match = None
|
||||
self._answered = False
|
||||
|
||||
def _set_client(self, client):
|
||||
super()._set_client(client)
|
||||
self._sender, self._input_sender = utils._get_entity_pair(
|
||||
self.sender_id, self._entities, client._mb_entity_cache)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""
|
||||
Returns the unique identifier for the query ID.
|
||||
"""
|
||||
return self.query.query_id
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
"""
|
||||
Returns the text the user used to make the inline query.
|
||||
"""
|
||||
return self.query.query
|
||||
|
||||
@property
|
||||
def offset(self):
|
||||
"""
|
||||
The string the user's client used as an offset for the query.
|
||||
This will either be empty or equal to offsets passed to `answer`.
|
||||
"""
|
||||
return self.query.offset
|
||||
|
||||
@property
|
||||
def geo(self):
|
||||
"""
|
||||
If the user location is requested when using inline mode
|
||||
and the user's device is able to send it, this will return
|
||||
the :tl:`GeoPoint` with the position of the user.
|
||||
"""
|
||||
return self.query.geo
|
||||
|
||||
@property
|
||||
def builder(self):
|
||||
"""
|
||||
Returns a new `InlineBuilder
|
||||
<telethon.tl.custom.inlinebuilder.InlineBuilder>` instance.
|
||||
"""
|
||||
return custom.InlineBuilder(self._client)
|
||||
|
||||
async def answer(
|
||||
self, results=None, cache_time=0, *,
|
||||
gallery=False, next_offset=None, private=False,
|
||||
switch_pm=None, switch_pm_param=''):
|
||||
"""
|
||||
Answers the inline query with the given results.
|
||||
|
||||
See the documentation for `builder` to know what kind of answers
|
||||
can be given.
|
||||
|
||||
Args:
|
||||
results (`list`, optional):
|
||||
A list of :tl:`InputBotInlineResult` to use.
|
||||
You should use `builder` to create these:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
builder = inline.builder
|
||||
r1 = builder.article('Be nice', text='Have a nice day')
|
||||
r2 = builder.article('Be bad', text="I don't like you")
|
||||
await inline.answer([r1, r2])
|
||||
|
||||
You can send up to 50 results as documented in
|
||||
https://core.telegram.org/bots/api#answerinlinequery.
|
||||
Sending more will raise ``ResultsTooMuchError``,
|
||||
and you should consider using `next_offset` to
|
||||
paginate them.
|
||||
|
||||
cache_time (`int`, optional):
|
||||
For how long this result should be cached on
|
||||
the user's client. Defaults to 0 for no cache.
|
||||
|
||||
gallery (`bool`, optional):
|
||||
Whether the results should show as a gallery (grid) or not.
|
||||
|
||||
next_offset (`str`, optional):
|
||||
The offset the client will send when the user scrolls the
|
||||
results and it repeats the request.
|
||||
|
||||
private (`bool`, optional):
|
||||
Whether the results should be cached by Telegram
|
||||
(not private) or by the user's client (private).
|
||||
|
||||
switch_pm (`str`, optional):
|
||||
If set, this text will be shown in the results
|
||||
to allow the user to switch to private messages.
|
||||
|
||||
switch_pm_param (`str`, optional):
|
||||
Optional parameter to start the bot with if
|
||||
`switch_pm` was used.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@bot.on(events.InlineQuery)
|
||||
async def handler(event):
|
||||
builder = event.builder
|
||||
|
||||
rev_text = event.text[::-1]
|
||||
await event.answer([
|
||||
builder.article('Reverse text', text=rev_text),
|
||||
builder.photo('/path/to/photo.jpg')
|
||||
])
|
||||
"""
|
||||
if self._answered:
|
||||
return
|
||||
|
||||
if results:
|
||||
futures = [self._as_future(x) for x in results]
|
||||
|
||||
await asyncio.wait(futures)
|
||||
|
||||
# All futures will be in the `done` *set* that `wait` returns.
|
||||
#
|
||||
# Precisely because it's a `set` and not a `list`, it
|
||||
# will not preserve the order, but since all futures
|
||||
# completed we can use our original, ordered `list`.
|
||||
results = [x.result() for x in futures]
|
||||
else:
|
||||
results = []
|
||||
|
||||
if switch_pm:
|
||||
switch_pm = types.InlineBotSwitchPM(switch_pm, switch_pm_param)
|
||||
|
||||
return await self._client(
|
||||
functions.messages.SetInlineBotResultsRequest(
|
||||
query_id=self.query.query_id,
|
||||
results=results,
|
||||
cache_time=cache_time,
|
||||
gallery=gallery,
|
||||
next_offset=next_offset,
|
||||
private=private,
|
||||
switch_pm=switch_pm
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _as_future(obj):
|
||||
if inspect.isawaitable(obj):
|
||||
return asyncio.ensure_future(obj)
|
||||
|
||||
f = helpers.get_running_loop().create_future()
|
||||
f.set_result(obj)
|
||||
return f
|
|
@ -1,57 +0,0 @@
|
|||
from .common import EventBuilder, EventCommon, name_inner_event
|
||||
from ..tl import types
|
||||
|
||||
|
||||
@name_inner_event
|
||||
class MessageDeleted(EventBuilder):
|
||||
"""
|
||||
Occurs whenever a message is deleted. Note that this event isn't 100%
|
||||
reliable, since Telegram doesn't always notify the clients that a message
|
||||
was deleted.
|
||||
|
||||
.. important::
|
||||
|
||||
Telegram **does not** send information about *where* a message
|
||||
was deleted if it occurs in private conversations with other users
|
||||
or in small group chats, because message IDs are *unique* and you
|
||||
can identify the chat with the message ID alone if you saved it
|
||||
previously.
|
||||
|
||||
Telethon **does not** save information of where messages occur,
|
||||
so it cannot know in which chat a message was deleted (this will
|
||||
only work in channels, where the channel ID *is* present).
|
||||
|
||||
This means that the ``chats=`` parameter will not work reliably,
|
||||
unless you intend on working with channels and super-groups only.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import events
|
||||
|
||||
@client.on(events.MessageDeleted)
|
||||
async def handler(event):
|
||||
# Log all deleted message IDs
|
||||
for msg_id in event.deleted_ids:
|
||||
print('Message', msg_id, 'was deleted in', event.chat_id)
|
||||
"""
|
||||
@classmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
if isinstance(update, types.UpdateDeleteMessages):
|
||||
return cls.Event(
|
||||
deleted_ids=update.messages,
|
||||
peer=None
|
||||
)
|
||||
elif isinstance(update, types.UpdateDeleteChannelMessages):
|
||||
return cls.Event(
|
||||
deleted_ids=update.messages,
|
||||
peer=types.PeerChannel(update.channel_id)
|
||||
)
|
||||
|
||||
class Event(EventCommon):
|
||||
def __init__(self, deleted_ids, peer):
|
||||
super().__init__(
|
||||
chat_peer=peer, msg_id=(deleted_ids or [0])[0]
|
||||
)
|
||||
self.deleted_id = None if not deleted_ids else deleted_ids[0]
|
||||
self.deleted_ids = deleted_ids
|
|
@ -1,52 +0,0 @@
|
|||
from .common import name_inner_event
|
||||
from .newmessage import NewMessage
|
||||
from ..tl import types
|
||||
|
||||
|
||||
@name_inner_event
|
||||
class MessageEdited(NewMessage):
|
||||
"""
|
||||
Occurs whenever a message is edited. Just like `NewMessage
|
||||
<telethon.events.newmessage.NewMessage>`, you should treat
|
||||
this event as a `Message <telethon.tl.custom.message.Message>`.
|
||||
|
||||
.. warning::
|
||||
|
||||
On channels, `Message.out <telethon.tl.custom.message.Message>`
|
||||
will be `True` if you sent the message originally, **not if
|
||||
you edited it**! This can be dangerous if you run outgoing
|
||||
commands on edits.
|
||||
|
||||
Some examples follow:
|
||||
|
||||
* You send a message "A", ``out is True``.
|
||||
* You edit "A" to "B", ``out is True``.
|
||||
* Someone else edits "B" to "C", ``out is True`` (**be careful!**).
|
||||
* Someone sends "X", ``out is False``.
|
||||
* Someone edits "X" to "Y", ``out is False``.
|
||||
* You edit "Y" to "Z", ``out is False``.
|
||||
|
||||
Since there are useful cases where you need the right ``out``
|
||||
value, the library cannot do anything automatically to help you.
|
||||
Instead, consider using ``from_users='me'`` (it won't work in
|
||||
broadcast channels at all since the sender is the channel and
|
||||
not you).
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import events
|
||||
|
||||
@client.on(events.MessageEdited)
|
||||
async def handler(event):
|
||||
# Log the date of new edits
|
||||
print('Message', event.id, 'changed at', event.date)
|
||||
"""
|
||||
@classmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
if isinstance(update, (types.UpdateEditMessage,
|
||||
types.UpdateEditChannelMessage)):
|
||||
return cls.Event(update.message)
|
||||
|
||||
class Event(NewMessage.Event):
|
||||
pass # Required if we want a different name for it
|
|
@ -1,143 +0,0 @@
|
|||
from .common import EventBuilder, EventCommon, name_inner_event
|
||||
from .. import utils
|
||||
from ..tl import types
|
||||
|
||||
|
||||
@name_inner_event
|
||||
class MessageRead(EventBuilder):
|
||||
"""
|
||||
Occurs whenever one or more messages are read in a chat.
|
||||
|
||||
Args:
|
||||
inbox (`bool`, optional):
|
||||
If this argument is `True`, then when you read someone else's
|
||||
messages the event will be fired. By default (`False`) only
|
||||
when messages you sent are read by someone else will fire it.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import events
|
||||
|
||||
@client.on(events.MessageRead)
|
||||
async def handler(event):
|
||||
# Log when someone reads your messages
|
||||
print('Someone has read all your messages until', event.max_id)
|
||||
|
||||
@client.on(events.MessageRead(inbox=True))
|
||||
async def handler(event):
|
||||
# Log when you read message in a chat (from your "inbox")
|
||||
print('You have read messages until', event.max_id)
|
||||
"""
|
||||
def __init__(
|
||||
self, chats=None, *, blacklist_chats=False, func=None, inbox=False):
|
||||
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
|
||||
self.inbox = inbox
|
||||
|
||||
@classmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
if isinstance(update, types.UpdateReadHistoryInbox):
|
||||
return cls.Event(update.peer, update.max_id, False)
|
||||
elif isinstance(update, types.UpdateReadHistoryOutbox):
|
||||
return cls.Event(update.peer, update.max_id, True)
|
||||
elif isinstance(update, types.UpdateReadChannelInbox):
|
||||
return cls.Event(types.PeerChannel(update.channel_id),
|
||||
update.max_id, False)
|
||||
elif isinstance(update, types.UpdateReadChannelOutbox):
|
||||
return cls.Event(types.PeerChannel(update.channel_id),
|
||||
update.max_id, True)
|
||||
elif isinstance(update, types.UpdateReadMessagesContents):
|
||||
return cls.Event(message_ids=update.messages,
|
||||
contents=True)
|
||||
elif isinstance(update, types.UpdateChannelReadMessagesContents):
|
||||
return cls.Event(types.PeerChannel(update.channel_id),
|
||||
message_ids=update.messages,
|
||||
contents=True)
|
||||
|
||||
def filter(self, event):
|
||||
if self.inbox == event.outbox:
|
||||
return
|
||||
|
||||
return super().filter(event)
|
||||
|
||||
class Event(EventCommon):
|
||||
"""
|
||||
Represents the event of one or more messages being read.
|
||||
|
||||
Members:
|
||||
max_id (`int`):
|
||||
Up to which message ID has been read. Every message
|
||||
with an ID equal or lower to it have been read.
|
||||
|
||||
outbox (`bool`):
|
||||
`True` if someone else has read your messages.
|
||||
|
||||
contents (`bool`):
|
||||
`True` if what was read were the contents of a message.
|
||||
This will be the case when e.g. you play a voice note.
|
||||
It may only be set on ``inbox`` events.
|
||||
"""
|
||||
def __init__(self, peer=None, max_id=None, out=False, contents=False,
|
||||
message_ids=None):
|
||||
self.outbox = out
|
||||
self.contents = contents
|
||||
self._message_ids = message_ids or []
|
||||
self._messages = None
|
||||
self.max_id = max_id or max(message_ids or [], default=None)
|
||||
super().__init__(peer, self.max_id)
|
||||
|
||||
@property
|
||||
def inbox(self):
|
||||
"""
|
||||
`True` if you have read someone else's messages.
|
||||
"""
|
||||
return not self.outbox
|
||||
|
||||
@property
|
||||
def message_ids(self):
|
||||
"""
|
||||
The IDs of the messages **which contents'** were read.
|
||||
|
||||
Use :meth:`is_read` if you need to check whether a message
|
||||
was read instead checking if it's in here.
|
||||
"""
|
||||
return self._message_ids
|
||||
|
||||
async def get_messages(self):
|
||||
"""
|
||||
Returns the list of `Message <telethon.tl.custom.message.Message>`
|
||||
**which contents'** were read.
|
||||
|
||||
Use :meth:`is_read` if you need to check whether a message
|
||||
was read instead checking if it's in here.
|
||||
"""
|
||||
if self._messages is None:
|
||||
chat = await self.get_input_chat()
|
||||
if not chat:
|
||||
self._messages = []
|
||||
else:
|
||||
self._messages = await self._client.get_messages(
|
||||
chat, ids=self._message_ids)
|
||||
|
||||
return self._messages
|
||||
|
||||
def is_read(self, message):
|
||||
"""
|
||||
Returns `True` if the given message (or its ID) has been read.
|
||||
|
||||
If a list-like argument is provided, this method will return a
|
||||
list of booleans indicating which messages have been read.
|
||||
"""
|
||||
if utils.is_list_like(message):
|
||||
return [(m if isinstance(m, int) else m.id) <= self.max_id
|
||||
for m in message]
|
||||
else:
|
||||
return (message if isinstance(message, int)
|
||||
else message.id) <= self.max_id
|
||||
|
||||
def __contains__(self, message):
|
||||
"""`True` if the message(s) are read message."""
|
||||
if utils.is_list_like(message):
|
||||
return all(self.is_read(message))
|
||||
else:
|
||||
return self.is_read(message)
|
|
@ -1,223 +0,0 @@
|
|||
import re
|
||||
|
||||
from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set
|
||||
from .. import utils
|
||||
from ..tl import types
|
||||
|
||||
|
||||
@name_inner_event
|
||||
class NewMessage(EventBuilder):
|
||||
"""
|
||||
Occurs whenever a new text message or a message with media arrives.
|
||||
|
||||
Args:
|
||||
incoming (`bool`, optional):
|
||||
If set to `True`, only **incoming** messages will be handled.
|
||||
Mutually exclusive with ``outgoing`` (can only set one of either).
|
||||
|
||||
outgoing (`bool`, optional):
|
||||
If set to `True`, only **outgoing** messages will be handled.
|
||||
Mutually exclusive with ``incoming`` (can only set one of either).
|
||||
|
||||
from_users (`entity`, optional):
|
||||
Unlike `chats`, this parameter filters the *senders* of the
|
||||
message. That is, only messages *sent by these users* will be
|
||||
handled. Use `chats` if you want private messages with this/these
|
||||
users. `from_users` lets you filter by messages sent by *one or
|
||||
more* users across the desired chats (doesn't need a list).
|
||||
|
||||
forwards (`bool`, optional):
|
||||
Whether forwarded messages should be handled or not. By default,
|
||||
both forwarded and normal messages are included. If it's `True`
|
||||
*only* forwards will be handled. If it's `False` only messages
|
||||
that are *not* forwards will be handled.
|
||||
|
||||
pattern (`str`, `callable`, `Pattern`, optional):
|
||||
If set, only messages matching this pattern will be handled.
|
||||
You can specify a regex-like string which will be matched
|
||||
against the message, a callable function that returns `True`
|
||||
if a message is acceptable, or a compiled regex pattern.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
import asyncio
|
||||
from telethon import events
|
||||
|
||||
@client.on(events.NewMessage(pattern='(?i)hello.+'))
|
||||
async def handler(event):
|
||||
# Respond whenever someone says "Hello" and something else
|
||||
await event.reply('Hey!')
|
||||
|
||||
@client.on(events.NewMessage(outgoing=True, pattern='!ping'))
|
||||
async def handler(event):
|
||||
# Say "!pong" whenever you send "!ping", then delete both messages
|
||||
m = await event.respond('!pong')
|
||||
await asyncio.sleep(5)
|
||||
await client.delete_messages(event.chat_id, [event.id, m.id])
|
||||
"""
|
||||
def __init__(self, chats=None, *, blacklist_chats=False, func=None,
|
||||
incoming=None, outgoing=None,
|
||||
from_users=None, forwards=None, pattern=None):
|
||||
if incoming and outgoing:
|
||||
incoming = outgoing = None # Same as no filter
|
||||
elif incoming is not None and outgoing is None:
|
||||
outgoing = not incoming
|
||||
elif outgoing is not None and incoming is None:
|
||||
incoming = not outgoing
|
||||
elif all(x is not None and not x for x in (incoming, outgoing)):
|
||||
raise ValueError("Don't create an event handler if you "
|
||||
"don't want neither incoming nor outgoing!")
|
||||
|
||||
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
|
||||
self.incoming = incoming
|
||||
self.outgoing = outgoing
|
||||
self.from_users = from_users
|
||||
self.forwards = forwards
|
||||
if isinstance(pattern, str):
|
||||
self.pattern = re.compile(pattern).match
|
||||
elif not pattern or callable(pattern):
|
||||
self.pattern = pattern
|
||||
elif hasattr(pattern, 'match') and callable(pattern.match):
|
||||
self.pattern = pattern.match
|
||||
else:
|
||||
raise TypeError('Invalid pattern type given')
|
||||
|
||||
# Should we short-circuit? E.g. perform no check at all
|
||||
self._no_check = all(x is None for x in (
|
||||
self.chats, self.incoming, self.outgoing, self.pattern,
|
||||
self.from_users, self.forwards, self.from_users, self.func
|
||||
))
|
||||
|
||||
async def _resolve(self, client):
|
||||
await super()._resolve(client)
|
||||
self.from_users = await _into_id_set(client, self.from_users)
|
||||
|
||||
@classmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
if isinstance(update,
|
||||
(types.UpdateNewMessage, types.UpdateNewChannelMessage)):
|
||||
if not isinstance(update.message, types.Message):
|
||||
return # We don't care about MessageService's here
|
||||
event = cls.Event(update.message)
|
||||
elif isinstance(update, types.UpdateShortMessage):
|
||||
event = cls.Event(types.Message(
|
||||
out=update.out,
|
||||
mentioned=update.mentioned,
|
||||
media_unread=update.media_unread,
|
||||
silent=update.silent,
|
||||
id=update.id,
|
||||
peer_id=types.PeerUser(update.user_id),
|
||||
from_id=types.PeerUser(self_id if update.out else update.user_id),
|
||||
message=update.message,
|
||||
date=update.date,
|
||||
fwd_from=update.fwd_from,
|
||||
via_bot_id=update.via_bot_id,
|
||||
reply_to=update.reply_to,
|
||||
entities=update.entities,
|
||||
ttl_period=update.ttl_period
|
||||
))
|
||||
elif isinstance(update, types.UpdateShortChatMessage):
|
||||
event = cls.Event(types.Message(
|
||||
out=update.out,
|
||||
mentioned=update.mentioned,
|
||||
media_unread=update.media_unread,
|
||||
silent=update.silent,
|
||||
id=update.id,
|
||||
from_id=types.PeerUser(self_id if update.out else update.from_id),
|
||||
peer_id=types.PeerChat(update.chat_id),
|
||||
message=update.message,
|
||||
date=update.date,
|
||||
fwd_from=update.fwd_from,
|
||||
via_bot_id=update.via_bot_id,
|
||||
reply_to=update.reply_to,
|
||||
entities=update.entities,
|
||||
ttl_period=update.ttl_period
|
||||
))
|
||||
else:
|
||||
return
|
||||
|
||||
return event
|
||||
|
||||
def filter(self, event):
|
||||
if self._no_check:
|
||||
return event
|
||||
|
||||
if self.incoming and event.message.out:
|
||||
return
|
||||
if self.outgoing and not event.message.out:
|
||||
return
|
||||
if self.forwards is not None:
|
||||
if bool(self.forwards) != bool(event.message.fwd_from):
|
||||
return
|
||||
|
||||
if self.from_users is not None:
|
||||
if event.message.sender_id not in self.from_users:
|
||||
return
|
||||
|
||||
if self.pattern:
|
||||
match = self.pattern(event.message.message or '')
|
||||
if not match:
|
||||
return
|
||||
event.pattern_match = match
|
||||
|
||||
return super().filter(event)
|
||||
|
||||
class Event(EventCommon):
|
||||
"""
|
||||
Represents the event of a new message. This event can be treated
|
||||
to all effects as a `Message <telethon.tl.custom.message.Message>`,
|
||||
so please **refer to its documentation** to know what you can do
|
||||
with this event.
|
||||
|
||||
Members:
|
||||
message (`Message <telethon.tl.custom.message.Message>`):
|
||||
This is the only difference with the received
|
||||
`Message <telethon.tl.custom.message.Message>`, and will
|
||||
return the `telethon.tl.custom.message.Message` itself,
|
||||
not the text.
|
||||
|
||||
See `Message <telethon.tl.custom.message.Message>` for
|
||||
the rest of available members and methods.
|
||||
|
||||
pattern_match (`obj`):
|
||||
The resulting object from calling the passed ``pattern`` function.
|
||||
Here's an example using a string (defaults to regex match):
|
||||
|
||||
>>> from telethon import TelegramClient, events
|
||||
>>> client = TelegramClient(...)
|
||||
>>>
|
||||
>>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!'))
|
||||
... async 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.
|
||||
... print('Welcomed', event.pattern_match.group(1))
|
||||
...
|
||||
>>>
|
||||
"""
|
||||
def __init__(self, message):
|
||||
self.__dict__['_init'] = False
|
||||
super().__init__(chat_peer=message.peer_id,
|
||||
msg_id=message.id, broadcast=bool(message.post))
|
||||
|
||||
self.pattern_match = None
|
||||
self.message = message
|
||||
|
||||
def _set_client(self, client):
|
||||
super()._set_client(client)
|
||||
m = self.message
|
||||
m._finish_init(client, self._entities, None)
|
||||
self.__dict__['_init'] = True # No new attributes can be set
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item in self.__dict__:
|
||||
return self.__dict__[item]
|
||||
else:
|
||||
return getattr(self.message, item)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if not self.__dict__['_init'] or name in self.__dict__:
|
||||
self.__dict__[name] = value
|
||||
else:
|
||||
setattr(self.message, name, value)
|
|
@ -1,53 +0,0 @@
|
|||
from .common import EventBuilder
|
||||
from .. import utils
|
||||
|
||||
|
||||
class Raw(EventBuilder):
|
||||
"""
|
||||
Raw events are not actual events. Instead, they are the raw
|
||||
:tl:`Update` object that Telegram sends. You normally shouldn't
|
||||
need these.
|
||||
|
||||
Args:
|
||||
types (`list` | `tuple` | `type`, optional):
|
||||
The type or types that the :tl:`Update` instance must be.
|
||||
Equivalent to ``if not isinstance(update, types): return``.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import events
|
||||
|
||||
@client.on(events.Raw)
|
||||
async def handler(update):
|
||||
# Print all incoming updates
|
||||
print(update.stringify())
|
||||
"""
|
||||
def __init__(self, types=None, *, func=None):
|
||||
super().__init__(func=func)
|
||||
if not types:
|
||||
self.types = None
|
||||
elif not utils.is_list_like(types):
|
||||
if not isinstance(types, type):
|
||||
raise TypeError('Invalid input type given: {}'.format(types))
|
||||
|
||||
self.types = types
|
||||
else:
|
||||
if not all(isinstance(x, type) for x in types):
|
||||
raise TypeError('Invalid input types given: {}'.format(types))
|
||||
|
||||
self.types = tuple(types)
|
||||
|
||||
async def resolve(self, client):
|
||||
self.resolved = True
|
||||
|
||||
@classmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
return update
|
||||
|
||||
def filter(self, event):
|
||||
if not self.types or isinstance(event, self.types):
|
||||
if self.func:
|
||||
# Return the result of func directly as it may need to be awaited
|
||||
return self.func(event)
|
||||
return event
|
|
@ -1,310 +0,0 @@
|
|||
import datetime
|
||||
import functools
|
||||
|
||||
from .common import EventBuilder, EventCommon, name_inner_event
|
||||
from .. import utils
|
||||
from ..tl import types
|
||||
from ..tl.custom.sendergetter import SenderGetter
|
||||
|
||||
|
||||
# TODO Either the properties are poorly named or they should be
|
||||
# different events, but that would be a breaking change.
|
||||
#
|
||||
# TODO There are more "user updates", but bundling them all up
|
||||
# in a single place will make it annoying to use (since
|
||||
# the user needs to check for the existence of `None`).
|
||||
#
|
||||
# TODO Handle UpdateUserBlocked, UpdateUserName, UpdateUserPhone, UpdateUser
|
||||
|
||||
def _requires_action(function):
|
||||
@functools.wraps(function)
|
||||
def wrapped(self):
|
||||
return None if self.action is None else function(self)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def _requires_status(function):
|
||||
@functools.wraps(function)
|
||||
def wrapped(self):
|
||||
return None if self.status is None else function(self)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
@name_inner_event
|
||||
class UserUpdate(EventBuilder):
|
||||
"""
|
||||
Occurs whenever a user goes online, starts typing, etc.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import events
|
||||
|
||||
@client.on(events.UserUpdate)
|
||||
async def handler(event):
|
||||
# If someone is uploading, say something
|
||||
if event.uploading:
|
||||
await client.send_message(event.user_id, 'What are you sending?')
|
||||
"""
|
||||
@classmethod
|
||||
def build(cls, update, others=None, self_id=None):
|
||||
if isinstance(update, types.UpdateUserStatus):
|
||||
return cls.Event(types.PeerUser(update.user_id),
|
||||
status=update.status)
|
||||
elif isinstance(update, types.UpdateChannelUserTyping):
|
||||
return cls.Event(update.from_id,
|
||||
chat_peer=types.PeerChannel(update.channel_id),
|
||||
typing=update.action)
|
||||
elif isinstance(update, types.UpdateChatUserTyping):
|
||||
return cls.Event(update.from_id,
|
||||
chat_peer=types.PeerChat(update.chat_id),
|
||||
typing=update.action)
|
||||
elif isinstance(update, types.UpdateUserTyping):
|
||||
return cls.Event(update.user_id,
|
||||
typing=update.action)
|
||||
|
||||
class Event(EventCommon, SenderGetter):
|
||||
"""
|
||||
Represents the event of a user update
|
||||
such as gone online, started typing, etc.
|
||||
|
||||
Members:
|
||||
status (:tl:`UserStatus`, optional):
|
||||
The user status if the update is about going online or offline.
|
||||
|
||||
You should check this attribute first before checking any
|
||||
of the seen within properties, since they will all be `None`
|
||||
if the status is not set.
|
||||
|
||||
action (:tl:`SendMessageAction`, optional):
|
||||
The "typing" action if any the user is performing if any.
|
||||
|
||||
You should check this attribute first before checking any
|
||||
of the typing properties, since they will all be `None`
|
||||
if the action is not set.
|
||||
"""
|
||||
def __init__(self, peer, *, status=None, chat_peer=None, typing=None):
|
||||
super().__init__(chat_peer or peer)
|
||||
SenderGetter.__init__(self, utils.get_peer_id(peer))
|
||||
|
||||
self.status = status
|
||||
self.action = typing
|
||||
|
||||
def _set_client(self, client):
|
||||
super()._set_client(client)
|
||||
self._sender, self._input_sender = utils._get_entity_pair(
|
||||
self.sender_id, self._entities, client._mb_entity_cache)
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
"""Alias for `sender <telethon.tl.custom.sendergetter.SenderGetter.sender>`."""
|
||||
return self.sender
|
||||
|
||||
async def get_user(self):
|
||||
"""Alias for `get_sender <telethon.tl.custom.sendergetter.SenderGetter.get_sender>`."""
|
||||
return await self.get_sender()
|
||||
|
||||
@property
|
||||
def input_user(self):
|
||||
"""Alias for `input_sender <telethon.tl.custom.sendergetter.SenderGetter.input_sender>`."""
|
||||
return self.input_sender
|
||||
|
||||
async def get_input_user(self):
|
||||
"""Alias for `get_input_sender <telethon.tl.custom.sendergetter.SenderGetter.get_input_sender>`."""
|
||||
return await self.get_input_sender()
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
"""Alias for `sender_id <telethon.tl.custom.sendergetter.SenderGetter.sender_id>`."""
|
||||
return self.sender_id
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def typing(self):
|
||||
"""
|
||||
`True` if the action is typing a message.
|
||||
"""
|
||||
return isinstance(self.action, types.SendMessageTypingAction)
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def uploading(self):
|
||||
"""
|
||||
`True` if the action is uploading something.
|
||||
"""
|
||||
return isinstance(self.action, (
|
||||
types.SendMessageChooseContactAction,
|
||||
types.SendMessageChooseStickerAction,
|
||||
types.SendMessageUploadAudioAction,
|
||||
types.SendMessageUploadDocumentAction,
|
||||
types.SendMessageUploadPhotoAction,
|
||||
types.SendMessageUploadRoundAction,
|
||||
types.SendMessageUploadVideoAction
|
||||
))
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def recording(self):
|
||||
"""
|
||||
`True` if the action is recording something.
|
||||
"""
|
||||
return isinstance(self.action, (
|
||||
types.SendMessageRecordAudioAction,
|
||||
types.SendMessageRecordRoundAction,
|
||||
types.SendMessageRecordVideoAction
|
||||
))
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def playing(self):
|
||||
"""
|
||||
`True` if the action is playing a game.
|
||||
"""
|
||||
return isinstance(self.action, types.SendMessageGamePlayAction)
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def cancel(self):
|
||||
"""
|
||||
`True` if the action was cancelling other actions.
|
||||
"""
|
||||
return isinstance(self.action, types.SendMessageCancelAction)
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def geo(self):
|
||||
"""
|
||||
`True` if what's being uploaded is a geo.
|
||||
"""
|
||||
return isinstance(self.action, types.SendMessageGeoLocationAction)
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def audio(self):
|
||||
"""
|
||||
`True` if what's being recorded/uploaded is an audio.
|
||||
"""
|
||||
return isinstance(self.action, (
|
||||
types.SendMessageRecordAudioAction,
|
||||
types.SendMessageUploadAudioAction
|
||||
))
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def round(self):
|
||||
"""
|
||||
`True` if what's being recorded/uploaded is a round video.
|
||||
"""
|
||||
return isinstance(self.action, (
|
||||
types.SendMessageRecordRoundAction,
|
||||
types.SendMessageUploadRoundAction
|
||||
))
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def video(self):
|
||||
"""
|
||||
`True` if what's being recorded/uploaded is an video.
|
||||
"""
|
||||
return isinstance(self.action, (
|
||||
types.SendMessageRecordVideoAction,
|
||||
types.SendMessageUploadVideoAction
|
||||
))
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def contact(self):
|
||||
"""
|
||||
`True` if what's being uploaded (selected) is a contact.
|
||||
"""
|
||||
return isinstance(self.action, types.SendMessageChooseContactAction)
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def document(self):
|
||||
"""
|
||||
`True` if what's being uploaded is document.
|
||||
"""
|
||||
return isinstance(self.action, types.SendMessageUploadDocumentAction)
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def sticker(self):
|
||||
"""
|
||||
`True` if what's being uploaded is a sticker.
|
||||
"""
|
||||
return isinstance(self.action, types.SendMessageChooseStickerAction)
|
||||
|
||||
@property
|
||||
@_requires_action
|
||||
def photo(self):
|
||||
"""
|
||||
`True` if what's being uploaded is a photo.
|
||||
"""
|
||||
return isinstance(self.action, types.SendMessageUploadPhotoAction)
|
||||
|
||||
@property
|
||||
@_requires_status
|
||||
def last_seen(self):
|
||||
"""
|
||||
Exact `datetime.datetime` when the user was last seen if known.
|
||||
"""
|
||||
if isinstance(self.status, types.UserStatusOffline):
|
||||
return self.status.was_online
|
||||
|
||||
@property
|
||||
@_requires_status
|
||||
def until(self):
|
||||
"""
|
||||
The `datetime.datetime` until when the user should appear online.
|
||||
"""
|
||||
if isinstance(self.status, types.UserStatusOnline):
|
||||
return self.status.expires
|
||||
|
||||
def _last_seen_delta(self):
|
||||
if isinstance(self.status, types.UserStatusOffline):
|
||||
return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online
|
||||
elif isinstance(self.status, types.UserStatusOnline):
|
||||
return datetime.timedelta(days=0)
|
||||
elif isinstance(self.status, types.UserStatusRecently):
|
||||
return datetime.timedelta(days=1)
|
||||
elif isinstance(self.status, types.UserStatusLastWeek):
|
||||
return datetime.timedelta(days=7)
|
||||
elif isinstance(self.status, types.UserStatusLastMonth):
|
||||
return datetime.timedelta(days=30)
|
||||
else:
|
||||
return datetime.timedelta(days=365)
|
||||
|
||||
@property
|
||||
@_requires_status
|
||||
def online(self):
|
||||
"""
|
||||
`True` if the user is currently online,
|
||||
"""
|
||||
return self._last_seen_delta() <= datetime.timedelta(days=0)
|
||||
|
||||
@property
|
||||
@_requires_status
|
||||
def recently(self):
|
||||
"""
|
||||
`True` if the user was seen within a day.
|
||||
"""
|
||||
return self._last_seen_delta() <= datetime.timedelta(days=1)
|
||||
|
||||
@property
|
||||
@_requires_status
|
||||
def within_weeks(self):
|
||||
"""
|
||||
`True` if the user was seen within 7 days.
|
||||
"""
|
||||
return self._last_seen_delta() <= datetime.timedelta(days=7)
|
||||
|
||||
@property
|
||||
@_requires_status
|
||||
def within_months(self):
|
||||
"""
|
||||
`True` if the user was seen within 30 days.
|
||||
"""
|
||||
return self._last_seen_delta() <= datetime.timedelta(days=30)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user