mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-02-03 13:14:31 +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__/
|
__pycache__/
|
||||||
/dist/
|
*.py[cod]
|
||||||
/build/
|
*$py.class
|
||||||
/*.egg-info/
|
.pytest_cache/
|
||||||
/readthedocs/_build/
|
|
||||||
/.tox/
|
|
||||||
|
|
||||||
# API reference docs
|
|
||||||
/docs/
|
|
||||||
|
|
||||||
# File used to manually test new changes, contains sensitive data
|
|
||||||
/example.py
|
|
||||||
|
|
|
@ -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
|
Telethon
|
||||||
========
|
========
|
||||||
|
|
||||||
.. epigraph::
|
.. epigraph::
|
||||||
|
|
||||||
⭐️ Thanks **everyone** who has starred the project, it means a lot!
|
⭐️ 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::
|
.. important::
|
||||||
|
|
||||||
If you have code using Telethon before its 1.0 version, you must
|
If you have code using Telethon before its 2.0 version, it is strongly
|
||||||
read `Compatibility and Convenience`_ to learn how to migrate.
|
recommended to read the Migration Guide section in the documentation.
|
||||||
As with any third-party library for Telegram, be careful not to
|
As with any third-party library for Telegram, be careful not to
|
||||||
break `Telegram's ToS`_ or `Telegram can ban the account`_.
|
break `Telegram's ToS`_ or `Telegram can ban the account`_.
|
||||||
|
|
||||||
|
|
||||||
What is this?
|
What is this?
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
@ -29,7 +31,7 @@ Installing
|
||||||
|
|
||||||
.. code-block:: sh
|
.. code-block:: sh
|
||||||
|
|
||||||
pip3 install telethon
|
pip install telethon
|
||||||
|
|
||||||
|
|
||||||
Creating a client
|
Creating a client
|
||||||
|
@ -44,8 +46,8 @@ Creating a client
|
||||||
api_id = 12345
|
api_id = 12345
|
||||||
api_hash = '0123456789abcdef0123456789abcdef'
|
api_hash = '0123456789abcdef0123456789abcdef'
|
||||||
|
|
||||||
client = TelegramClient('session_name', api_id, api_hash)
|
async with TelegramClient('session_name', api_id, api_hash) as client:
|
||||||
client.start()
|
...
|
||||||
|
|
||||||
|
|
||||||
Doing stuff
|
Doing stuff
|
||||||
|
@ -53,34 +55,28 @@ Doing stuff
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
print(client.get_me().stringify())
|
print(await client.get_me())
|
||||||
|
|
||||||
client.send_message('username', 'Hello! Talking to you from Telethon')
|
await client.send_message('username', 'Hello! Talking to you from Telethon')
|
||||||
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
|
await client.send_message('username', photo='/home/myself/Pictures/holidays.jpg')
|
||||||
|
|
||||||
client.download_profile_photo('me')
|
async for message in client.get_messages('username', 1):
|
||||||
messages = client.get_messages('username')
|
path = await message.download_media()
|
||||||
messages[0].download_media()
|
print('Saved media to', path)
|
||||||
|
|
||||||
@client.on(events.NewMessage(pattern='(?i)hi|hello'))
|
|
||||||
async def handler(event):
|
|
||||||
await event.respond('Hey!')
|
|
||||||
|
|
||||||
|
|
||||||
Next steps
|
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
|
in-depth explanation, with examples, troubleshooting issues, and more
|
||||||
useful information.
|
useful information.
|
||||||
|
|
||||||
.. _asyncio: https://docs.python.org/3/library/asyncio.html
|
.. _asyncio: https://docs.python.org/3/library/asyncio.html
|
||||||
.. _MTProto: https://core.telegram.org/mtproto
|
.. _MTProto: https://core.telegram.org/mtproto
|
||||||
.. _Telegram: https://telegram.org
|
.. _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'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
|
.. _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
|
.. |logo| image:: logo.svg
|
||||||
:width: 24pt
|
: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