Compare commits

..

26 Commits

Author SHA1 Message Date
Lonami Exo
6a785a01aa Stop collapsing messages into containers
Apparently the bug where a list would come off the queue
would still happen sometimes. Just give up doing such thing
in the threaded version to avoid the bug alltogether.
2018-08-06 16:36:28 +02:00
Lonami Exo
5731cf015e Fix timeout not occuring where expected 2018-08-06 15:33:06 +02:00
Lonami Exo
4e985c3e4a Remove accidentally committed changes 2018-08-02 22:36:29 +02:00
Lonami Exo
3177e3a956 Merge branch 'master' into sync 2018-08-01 15:18:44 +02:00
Lonami Exo
b17768dd6e Remove loop= keyword (#477) 2018-07-22 20:11:55 +02:00
Lonami Exo
472e5fa444 Fix infinite recursion for custom.Message.message 2018-07-20 12:24:48 +02:00
Lonami Exo
c3692fd124 Fix infinite recursion 2018-07-20 00:01:01 +02:00
Lonami Exo
8a287a38ec Fix missing blocking result() call on connect (#888) 2018-07-10 21:25:05 +02:00
Lonami Exo
2efd78000f Fix not all items being put back in sync (#886) 2018-07-10 09:51:27 +02:00
Lonami Exo
1f7e8ad279 Remove missing async/await 2018-07-09 12:49:07 +02:00
Lonami Exo
71d2907017 Merge branch 'master' into sync 2018-07-09 12:47:13 +02:00
Lonami Exo
55789ca327 Work around a bug where queue.Queue returns never-inserted lists 2018-07-06 13:11:32 +02:00
Lonami Exo
37570d8eee Merge branch 'master' into sync 2018-07-04 15:44:25 +02:00
Lonami Exo
893a7a66b8 Also block when exporting authorization 2018-07-04 15:28:55 +02:00
Lonami Exo
78167b3c7b Fix downloads not blocking for a result 2018-07-04 15:25:14 +02:00
Lonami Exo
a9e4760216 Merge branch 'master' into sync 2018-06-29 11:35:39 +02:00
Lonami Exo
84f42795f0 Make sequential_updates=True the default in sync 2018-06-29 11:11:38 +02:00
Lonami Exo
e94ad7ad77 Merge branch 'master' into sync 2018-06-29 11:09:28 +02:00
Lonami Exo
d0ebb7790c Merge branch 'master' into sync 2018-06-28 15:50:46 +02:00
Lonami Exo
cb26b96375 Merge branch 'master' into sync 2018-06-28 15:38:13 +02:00
Lonami Exo
f41b41696a Fix generators 2018-06-28 15:35:25 +02:00
Lonami Exo
e1f8807d83 Fix sleeps 2018-06-28 15:22:22 +02:00
Lonami Exo
25b220b4bd Merge branch 'master' into sync 2018-06-28 15:16:09 +02:00
Lonami Exo
6b2088873b Fix updates 2018-06-28 09:34:56 +02:00
Lonami Exo
268e43d5c3 Use concurrent futures and threads 2018-06-28 09:29:55 +02:00
Lonami Exo
62c6565189 Remove all async/await 2018-06-28 09:08:18 +02:00
226 changed files with 7460 additions and 35344 deletions

View File

@ -1,8 +0,0 @@
[run]
branch = true
parallel = true
source =
telethon
[report]
precision = 2

10
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,10 @@
<!--
0. The library is Python >= 3.4, not Python 2.x.
1. If you have a QUESTION, ask it on @TelethonChat (Telegram) or StackOverflow, not here. It will be closed immediatly with no explanation if you post it here.
2. If you have an ISSUE or you are experiencing strange behaviour, make sure you're using the latest version (pip install -U telethon), and post as much information as possible here. Enhancement suggestions are welcome too.
If you paste code, please put it between three backticks (`):
```python
code here
```
-->

View File

@ -1,96 +0,0 @@
name: Bug Report
description: Create a report about a bug inside the library.
body:
- type: textarea
id: reproducing-example
attributes:
label: Code that causes the issue
description: Provide a code example that reproduces the problem. Try to keep it short without other dependencies.
placeholder: |
```python
from telethon.sync import TelegramClient
...
```
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: Explain what you should expect to happen. Include reproduction steps.
placeholder: |
"I was doing... I was expecting the following to happen..."
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
description: Explain what actually happens.
placeholder: |
"This happened instead..."
validations:
required: true
- type: textarea
id: traceback
attributes:
label: Traceback
description: |
The traceback, if the problem is a crash.
placeholder: |
```
Traceback (most recent call last):
File "code.py", line 1, in <code>
```
- type: input
id: telethon-version
attributes:
label: Telethon version
description: The output of `python -c "import telethon; print(telethon.__version__)"`.
placeholder: "1.x"
validations:
required: true
- type: input
id: python-version
attributes:
label: Python version
description: The output of `python --version`.
placeholder: "3.x"
validations:
required: true
- type: input
id: os
attributes:
label: Operating system (including distribution name and version)
placeholder: Windows 11, macOS 13.4, Ubuntu 23.04...
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments. Is it a server? Network condition?
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: The error is in the library's code, and not in my own.
required: true
- label: I have searched for this issue before posting it and there isn't an open duplicate.
required: true
- label: I ran `pip install -U https://github.com/LonamiWebs/Telethon/archive/v1.zip` and triggered the bug in the latest version.
required: true

View File

@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Ask questions in StackOverflow
url: https://stackoverflow.com/questions/ask?tags=telethon
about: Questions are not bugs. Please ask them in StackOverflow instead. Questions in the bug tracker will be closed
- name: Find about updates and our Telegram groups
url: https://t.me/s/TelethonUpdates
about: Be notified of updates, chat with other people about the library or ask questions in these groups

View File

@ -1,22 +0,0 @@
name: Documentation Issue
description: Report a problem with the documentation.
labels: [documentation]
body:
- type: textarea
id: description
attributes:
label: Description
description: Describe the problem in detail.
placeholder: This part is unclear...
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: This is a documentation problem, not a question or a bug report.
required: true
- label: I have searched for this issue before posting it and there isn't a duplicate.
required: true

View File

@ -1,22 +0,0 @@
name: Feature Request
description: Suggest ideas, changes or other enhancements for the library.
labels: [enhancement]
body:
- type: textarea
id: feature-description
attributes:
label: Describe your suggested feature
description: Please describe your idea. Would you like another friendly method? Renaming them to something more appropriate? Changing the way something works?
placeholder: "It should work like this..."
validations:
required: true
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: I have searched for this issue before posting it and there isn't a duplicate.
required: true

View File

@ -1,5 +0,0 @@
<!--
Thanks for the PR! Please keep in mind that v1 is *feature frozen*.
New features very likely won't be merged, although fixes can be sent.
All new development should happen in v2. Thanks!
-->

View File

@ -1,28 +0,0 @@
name: Python Library
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.5", "3.6", "3.7", "3.8"]
steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Set up env
run: |
python -m pip install --upgrade pip
pip install tox
- name: Lint with flake8
run: |
tox -e flake
- name: Test with pytest
run: |
# use "py", which is the default python version
tox -e py

116
.gitignore vendored Normal file → Executable file
View File

@ -1,23 +1,109 @@
# Docs
_build/
docs/
# Generated code # Generated code
/telethon/tl/functions/ telethon/tl/functions/
/telethon/tl/types/ telethon/tl/types/
/telethon/tl/alltlobjects.py telethon/tl/patched/
/telethon/errors/rpcerrorlist.py telethon/tl/alltlobjects.py
telethon/errors/rpcerrorlist.py
# User session # User session
*.session *.session
/usermedia/ usermedia/
api/settings
# Builds and testing # Quick tests should live in this file
example.py
# Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
/dist/ *.py[cod]
/build/ *$py.class
/*.egg-info/
/readthedocs/_build/
/.tox/
# API reference docs # C extensions
/docs/ *.so
# File used to manually test new changes, contains sensitive data # Distribution / packaging
/example.py .Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
.venv/
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject

View File

@ -1,18 +0,0 @@
# https://docs.readthedocs.io/en/stable/config-file/v2.html
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
configuration: readthedocs/conf.py
formats:
- pdf
- epub
python:
install:
- requirements: readthedocs/requirements.txt

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2016-Present LonamiWebs Copyright (c) 2016 LonamiWebs
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

4
MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
include LICENSE
include README.rst
recursive-include telethon *

View File

@ -4,84 +4,13 @@ Telethon
⭐️ Thanks **everyone** who has starred the project, it means a lot! ⭐️ Thanks **everyone** who has starred the project, it means a lot!
|logo| **Telethon** is an asyncio_ **Python 3** This is the threaded, simpler version of Telethon for people who
MTProto_ library to interact with Telegram_'s API can't bother learning ``asyncio`` but wouldn't like their scripts
as a user or through a bot account (bot API alternative). to just stop working. This version is also compatible with Python
3.4 but doesn't have any of the benefits of the ``asyncio`` version
and will receive updates slower.
.. important:: Please consider learning ``asyncio``. The `documentation
<http://telethon.rtfd.io/>`_ is the same for both versions
If you have code using Telethon before its 1.0 version, you must of the library. Simply don't write any keywords like ``async``
read `Compatibility and Convenience`_ to learn how to migrate. or ``await`` and you will be good.
As with any third-party library for Telegram, be careful not to
break `Telegram's ToS`_ or `Telegram can ban the account`_.
What is this?
-------------
Telegram is a popular messaging application. This library is meant
to make it easy for you to write Python programs that can interact
with Telegram. Think of it as a wrapper that has already done the
heavy job for you, so you can focus on developing an application.
Installing
----------
.. code-block:: sh
pip3 install telethon
Creating a client
-----------------
.. code-block:: python
from telethon import TelegramClient, events, sync
# These example values won't work. You must get your own api_id and
# api_hash from https://my.telegram.org, under API Development.
api_id = 12345
api_hash = '0123456789abcdef0123456789abcdef'
client = TelegramClient('session_name', api_id, api_hash)
client.start()
Doing stuff
-----------
.. code-block:: python
print(client.get_me().stringify())
client.send_message('username', 'Hello! Talking to you from Telethon')
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
client.download_profile_photo('me')
messages = client.get_messages('username')
messages[0].download_media()
@client.on(events.NewMessage(pattern='(?i)hi|hello'))
async def handler(event):
await event.respond('Hey!')
Next steps
----------
Do you like how Telethon looks? Check out `Read The Docs`_ for a more
in-depth explanation, with examples, troubleshooting issues, and more
useful information.
.. _asyncio: https://docs.python.org/3/library/asyncio.html
.. _MTProto: https://core.telegram.org/mtproto
.. _Telegram: https://telegram.org
.. _Compatibility and Convenience: https://docs.telethon.dev/en/stable/misc/compatibility-and-convenience.html
.. _Telegram's ToS: https://core.telegram.org/api/terms
.. _Telegram can ban the account: https://docs.telethon.dev/en/stable/quick-references/faq.html#my-account-was-deleted-limited-when-using-the-library
.. _Read The Docs: https://docs.telethon.dev
.. |logo| image:: logo.svg
:width: 24pt
:height: 24pt

4
api/settings_example Normal file
View File

@ -0,0 +1,4 @@
api_id=12345
api_hash=0123456789abcdef0123456789abcdef
user_phone=+34600000000
session_name=anonymous

View File

@ -1,3 +0,0 @@
pytest
pytest-cov
pytest-asyncio

View File

@ -1,6 +1,3 @@
cryptg cryptg
pysocks pysocks
python-socks[asyncio] hachoir3
hachoir
pillow
isal

View File

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

View File

@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = Telethon
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +0,0 @@
============
Coding Style
============
Basically, make it **readable**, while keeping the style similar to the
code of whatever file you're working on.
Also note that not everyone has 4K screens for their primary monitors,
so please try to stick to the 80-columns limit. This makes it easy to
``git diff`` changes from a terminal before committing changes. If the
line has to be long, please don't exceed 120 characters.
For the commit messages, please make them *explanatory*. Not only
they're helpful to troubleshoot when certain issues could have been
introduced, but they're also used to construct the change log once a new
version is ready.
If you don't know enough Python, I strongly recommend reading `Dive Into
Python 3 <http://www.diveintopython3.net/>`__, available online for
free. For instance, remember to do ``if x is None`` or
``if x is not None`` instead ``if x == None``!

View File

@ -1,25 +0,0 @@
==========
Philosophy
==========
The intention of the library is to have an existing MTProto library
existing with hardly any dependencies (indeed, wherever Python is
available, you can run this library).
Being written in Python means that performance will be nowhere close to
other implementations written in, for instance, Java, C++, Rust, or
pretty much any other compiled language. However, the library turns out
to actually be pretty decent for common operations such as sending
messages, receiving updates, or other scripting. Uploading files may be
notably slower, but if you would like to contribute, pull requests are
appreciated!
If ``libssl`` is available on your system, the library will make use of
it to speed up some critical parts such as encrypting and decrypting the
messages. Files will notably be sent and downloaded faster.
The main focus is to keep everything clean and simple, for everyone to
understand how working with MTProto and Telegram works. Don't be afraid
to read the source, the code won't bite you! It may prove useful when
using the library on your own use cases.

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +0,0 @@
============================
Tips for Porting the Project
============================
If you're going to use the code on this repository to guide you, please
be kind and don't forget to mention it helped you!
You should start by reading the source code on the `first
release <https://github.com/LonamiWebs/Telethon/releases/tag/v0.1>`__ of
the project, and start creating a ``MTProtoSender``. Once this is made,
you should write by hand the code to authenticate on the Telegram's
server, which are some steps required to get the key required to talk to
them. Save it somewhere! Then, simply mimic, or reinvent other parts of
the code, and it will be ready to go within a few days.
Good luck!

View File

@ -1,33 +0,0 @@
===============================
Understanding the Type Language
===============================
`Telegram's Type Language <https://core.telegram.org/mtproto/TL>`__
(also known as TL, found on ``.tl`` files) is a concise way to define
what other programming languages commonly call classes or structs.
Every definition is written as follows for a Telegram object is defined
as follows:
``name#id argument_name:argument_type = CommonType``
This means that in a single line you know what the ``TLObject`` name is.
You know it's unique ID, and you know what arguments it has. It really
isn't that hard to write a generator for generating code to any
platform!
The generated code should also be able to *encode* the ``TLObject`` (let
this be a request or a type) into bytes, so they can be sent over the
network. This isn't a big deal either, because you know how the
``TLObject``\ 's are made, and how the types should be serialized.
You can either write your own code generator, or use the one this
library provides, but please be kind and keep some special mention to
this project for helping you out.
This is only a introduction. The ``TL`` language is not *that* easy. But
it's not that hard either. You're free to sniff the
``telethon_generator/`` files and learn how to parse other more complex
lines, such as ``flags`` (to indicate things that may or may not be
written at all) and ``vector``\ 's.

View File

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

View File

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

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

View File

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

View File

@ -1,119 +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/compatibility-and-convenience
.. toctree::
:hidden:
:caption: Telethon Modules
modules/client
modules/events
modules/custom
modules/utils
modules/errors
modules/sessions
modules/network
modules/helpers

View File

@ -1,36 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=Telethon
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
=======
Helpers
=======
.. automodule:: telethon.helpers
:members:
:undoc-members:
:show-inheritance:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
./
sphinx-rtd-theme~=1.3.0

View File

@ -1,2 +1,3 @@
pyaes pyaes
rsa rsa
typing

24
run_tests.py Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
import unittest
if __name__ == '__main__':
from telethon_tests import \
CryptoTests, ParserTests, TLTests, UtilsTests, NetworkTests
test_classes = [CryptoTests, ParserTests, TLTests, UtilsTests]
network = input('Run network tests (y/n)?: ').lower() == 'y'
if network:
test_classes.append(NetworkTests)
loader = unittest.TestLoader()
suites_list = []
for test_class in test_classes:
suite = loader.loadTestsFromTestCase(test_class)
suites_list.append(suite)
big_suite = unittest.TestSuite(suites_list)
runner = unittest.TextTestRunner()
results = runner.run(big_suite)

165
setup.py
View File

@ -15,75 +15,68 @@ import json
import os import os
import re import re
import shutil import shutil
import sys from codecs import open
import urllib.request from sys import argv
from pathlib import Path
from subprocess import run
from setuptools import find_packages, setup from setuptools import find_packages, setup
# Needed since we're importing local files
sys.path.insert(0, os.path.dirname(__file__))
class TempWorkDir: class TempWorkDir:
"""Switches the working directory to be the one on which this file lives, """Switches the working directory to be the one on which this file lives,
while within the 'with' block. while within the 'with' block.
""" """
def __init__(self, new=None): def __init__(self):
self.original = None self.original = None
self.new = new or str(Path(__file__).parent.resolve())
def __enter__(self): def __enter__(self):
# os.chdir does not work with Path in Python 3.5.x self.original = os.path.abspath(os.path.curdir)
self.original = str(Path('.').resolve()) os.chdir(os.path.abspath(os.path.dirname(__file__)))
os.makedirs(self.new, exist_ok=True)
os.chdir(self.new)
return self return self
def __exit__(self, *args): def __exit__(self, *args):
os.chdir(self.original) os.chdir(self.original)
API_REF_URL = 'https://tl.telethon.dev/' GENERATOR_DIR = 'telethon_generator'
LIBRARY_DIR = 'telethon'
GENERATOR_DIR = Path('telethon_generator') ERRORS_IN_JSON = os.path.join(GENERATOR_DIR, 'data', 'errors.json')
LIBRARY_DIR = Path('telethon') ERRORS_IN_DESC = os.path.join(GENERATOR_DIR, 'data', 'error_descriptions')
ERRORS_OUT = os.path.join(LIBRARY_DIR, 'errors', 'rpcerrorlist.py')
ERRORS_IN = GENERATOR_DIR / 'data/errors.csv' INVALID_BM_IN = os.path.join(GENERATOR_DIR, 'data', 'invalid_bot_methods.json')
ERRORS_OUT = LIBRARY_DIR / 'errors/rpcerrorlist.py'
METHODS_IN = GENERATOR_DIR / 'data/methods.csv' TLOBJECT_IN_CORE_TL = os.path.join(GENERATOR_DIR, 'data', 'mtproto_api.tl')
TLOBJECT_IN_TL = os.path.join(GENERATOR_DIR, 'data', 'telegram_api.tl')
# Which raw API methods are covered by *friendly* methods in the client? TLOBJECT_OUT = os.path.join(LIBRARY_DIR, 'tl')
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 IMPORT_DEPTH = 2
DOCS_IN_RES = GENERATOR_DIR / 'data/html' DOCS_IN_RES = os.path.join(GENERATOR_DIR, 'data', 'html')
DOCS_OUT = Path('docs') DOCS_OUT = 'docs'
def generate(which, action='gen'): def generate(which):
from telethon_generator.parsers import\ from telethon_generator.parsers import parse_errors, parse_tl, find_layer
parse_errors, parse_methods, parse_tl, find_layer
from telethon_generator.generators import\ from telethon_generator.generators import\
generate_errors, generate_tlobjects, generate_docs, clean_tlobjects generate_errors, generate_tlobjects, generate_docs, clean_tlobjects
layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS))) # Older Python versions open the file as bytes instead (3.4.2)
errors = list(parse_errors(ERRORS_IN)) with open(INVALID_BM_IN, 'r') as f:
methods = list(parse_methods(METHODS_IN, FRIENDLY_IN, {e.str_code: e for e in errors})) invalid_bot_methods = set(json.load(f))
tlobjects = list(itertools.chain(*( layer = find_layer(TLOBJECT_IN_TL)
parse_tl(file, layer, methods) for file in TLOBJECT_IN_TLS))) errors = list(parse_errors(ERRORS_IN_JSON, ERRORS_IN_DESC))
tlobjects = list(itertools.chain(
parse_tl(TLOBJECT_IN_CORE_TL, layer, invalid_bot_methods),
parse_tl(TLOBJECT_IN_TL, layer, invalid_bot_methods)))
if not which: if not which:
which.extend(('tl', 'errors')) which.extend(('tl', 'errors'))
clean = action == 'clean' clean = 'clean' in which
action = 'Cleaning' if clean else 'Generating' action = 'Cleaning' if clean else 'Generating'
if clean:
which.remove('clean')
if 'all' in which: if 'all' in which:
which.remove('all') which.remove('all')
@ -103,105 +96,84 @@ def generate(which, action='gen'):
which.remove('errors') which.remove('errors')
print(action, 'RPCErrors...') print(action, 'RPCErrors...')
if clean: if clean:
if ERRORS_OUT.is_file(): if os.path.isfile(ERRORS_OUT):
ERRORS_OUT.unlink() os.remove(ERRORS_OUT)
else: else:
with ERRORS_OUT.open('w') as file: with open(ERRORS_OUT, 'w', encoding='utf-8') as file:
generate_errors(errors, file) generate_errors(errors, file)
if 'docs' in which: if 'docs' in which:
which.remove('docs') which.remove('docs')
print(action, 'documentation...') print(action, 'documentation...')
if clean: if clean:
if DOCS_OUT.is_dir(): if os.path.isdir(DOCS_OUT):
shutil.rmtree(str(DOCS_OUT)) shutil.rmtree(DOCS_OUT)
else: else:
in_path = DOCS_IN_RES.resolve() generate_docs(tlobjects, errors, layer, DOCS_IN_RES, DOCS_OUT)
with TempWorkDir(DOCS_OUT):
generate_docs(tlobjects, methods, layer, in_path)
if 'json' in which: if 'json' in which:
which.remove('json') which.remove('json')
print(action, 'JSON schema...') print(action, 'JSON schema...')
json_files = [x.with_suffix('.json') for x in TLOBJECT_IN_TLS] mtproto = 'mtproto_api.json'
telegram = 'telegram_api.json'
if clean: if clean:
for file in json_files: for x in (mtproto, telegram):
if file.is_file(): if os.path.isfile(x):
file.unlink() os.remove(x)
else: else:
def gen_json(fin, fout): def gen_json(fin, fout):
meths = [] methods = []
constructors = [] constructors = []
for tl in parse_tl(fin, layer): for tl in parse_tl(fin, layer):
if tl.is_function: if tl.is_function:
meths.append(tl.to_dict()) methods.append(tl.to_dict())
else: else:
constructors.append(tl.to_dict()) constructors.append(tl.to_dict())
what = {'constructors': constructors, 'methods': meths} what = {'constructors': constructors, 'methods': methods}
with open(fout, 'w') as f: with open(fout, 'w') as f:
json.dump(what, f, indent=2) json.dump(what, f, indent=2)
for fs in zip(TLOBJECT_IN_TLS, json_files): gen_json(TLOBJECT_IN_CORE_TL, mtproto)
gen_json(*fs) gen_json(TLOBJECT_IN_TL, telegram)
if which: if which:
print( print('The following items were not understood:', which)
'The following items were not understood:', which, print(' Consider using only "tl", "errors" and/or "docs".')
'\n Consider using only "tl", "errors" and/or "docs".' print(' Using only "clean" will clean them. "all" to act on all.')
'\n Using only "clean" will clean them. "all" to act on all.' print(' For instance "gen tl errors".')
'\n For instance "gen tl errors".'
)
def main(argv): def main():
if len(argv) >= 2 and argv[1] in ('gen', 'clean'): if len(argv) >= 2 and argv[1] == 'gen':
generate(argv[2:], argv[1]) generate(argv[2:])
elif len(argv) >= 2 and argv[1] == 'pypi': 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 # (Re)generate the code to make sure we don't push without it
generate(['tl', 'errors']) generate(['tl', 'errors'])
# Try importing the telethon module to assert it has no errors # Try importing the telethon module to assert it has no errors
try: try:
import telethon import telethon
except Exception as e: except:
print('Packaging for PyPi aborted, importing the module failed.') print('Packaging for PyPi aborted, importing the module failed.')
print(e)
return return
remove_dirs = ['__pycache__', 'build', 'dist', 'Telethon.egg-info'] # Need python3.5 or higher, but Telethon is supposed to support 3.x
for root, _dirs, _files in os.walk(LIBRARY_DIR, topdown=False): # Place it here since noone should be running ./setup.py pypi anyway
# setuptools is including __pycache__ for some reason (#1605) from subprocess import run
if root.endswith('/__pycache__'): from shutil import rmtree
remove_dirs.append(root)
for x in remove_dirs:
shutil.rmtree(x, ignore_errors=True)
for x in ('build', 'dist', 'Telethon.egg-info'):
rmtree(x, ignore_errors=True)
run('python3 setup.py sdist', shell=True) run('python3 setup.py sdist', shell=True)
run('python3 setup.py bdist_wheel', shell=True) run('python3 setup.py bdist_wheel', shell=True)
run('twine upload dist/*', shell=True) run('twine upload dist/*', shell=True)
for x in ('build', 'dist', 'Telethon.egg-info'): for x in ('build', 'dist', 'Telethon.egg-info'):
shutil.rmtree(x, ignore_errors=True) rmtree(x, ignore_errors=True)
else: else:
# e.g. install from GitHub # e.g. install from GitHub
if GENERATOR_DIR.is_dir(): if os.path.isdir(GENERATOR_DIR):
generate(['tl', 'errors']) generate(['tl', 'errors'])
# Get the long description from the README file # Get the long description from the README file
@ -212,7 +184,7 @@ def main(argv):
version = re.search(r"^__version__\s*=\s*'(.*)'.*$", version = re.search(r"^__version__\s*=\s*'(.*)'.*$",
f.read(), flags=re.MULTILINE).group(1) f.read(), flags=re.MULTILINE).group(1)
setup( setup(
name='Telethon', name='Telethon-sync',
version=version, version=version,
description="Full-featured Telegram client library for Python 3", description="Full-featured Telegram client library for Python 3",
long_description=long_description, long_description=long_description,
@ -228,7 +200,7 @@ def main(argv):
# See https://stackoverflow.com/a/40300957/4759433 # See https://stackoverflow.com/a/40300957/4759433
# -> https://www.python.org/dev/peps/pep-0345/#requires-python # -> https://www.python.org/dev/peps/pep-0345/#requires-python
# -> http://setuptools.readthedocs.io/en/latest/setuptools.html # -> http://setuptools.readthedocs.io/en/latest/setuptools.html
python_requires='>=3.5', python_requires='>=3.4',
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[ classifiers=[
@ -243,14 +215,13 @@ def main(argv):
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6'
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
], ],
keywords='telegram api chat client library messaging mtproto', keywords='telegram api chat client library messaging mtproto',
packages=find_packages(exclude=[ packages=find_packages(exclude=[
'telethon_*', 'tests*' 'telethon_*', 'run_tests.py', 'try_telethon.py'
]), ]),
install_requires=['pyaes', 'rsa'], install_requires=['pyaes', 'rsa'],
extras_require={ extras_require={
@ -260,5 +231,5 @@ def main(argv):
if __name__ == '__main__': if __name__ == '__main__':
with TempWorkDir(): with TempWorkDir(): # Could just use a try/finally but this is + reusable
main(sys.argv) main()

View File

@ -1,13 +1,12 @@
import logging
from .client.telegramclient import TelegramClient from .client.telegramclient import TelegramClient
from .network import connection from .network import connection
from .tl.custom import Button from .tl import types, functions, custom
from .tl import patched as _ # import for its side-effects from . import version, events, utils, errors
from . import version, events, utils, errors, types, functions, custom
__version__ = version.__version__ __version__ = version.__version__
logging.getLogger(__name__).addHandler(logging.NullHandler())
__all__ = [ __all__ = ['TelegramClient', 'types', 'functions', 'custom',
'TelegramClient', 'Button', 'events', 'utils', 'errors']
'types', 'functions', 'custom', 'errors',
'events', 'utils', 'connection'
]

View File

@ -1,3 +0,0 @@
from .entitycache import EntityCache
from .messagebox import MessageBox, GapError, PrematureEndReason
from .session import SessionState, ChannelState, Entity, EntityType

View File

@ -1,59 +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 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)

View File

@ -1,825 +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
# object() but with a tag to make it easier to debug
class Sentinel:
__slots__ = ('tag',)
def __init__(self, tag=None):
self.tag = tag or '_'
def __repr__(self):
return self.tag
# Entry "enum".
# Account-wide `pts` includes private conversations (one-to-one) and small group chats.
ENTRY_ACCOUNT = Sentinel('ACCOUNT')
# Account-wide `qts` includes only "secret" one-to-one chats.
ENTRY_SECRET = Sentinel('SECRET')
# 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 = Sentinel()
def next_updates_deadline():
return get_running_loop().time() + NO_UPDATES_TIMEOUT
def epoch():
return datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc)
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:
return cls(pts=qts, pts_count=1, entry=ENTRY_SECRET)
return None
def __repr__(self):
return f'PtsInfo(pts={self.pts}, pts_count={self.pts_count}, entry={self.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')
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 = epoch() + datetime.timedelta(seconds=1),
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
):
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
if __debug__:
self._trace('MessageBox initialized')
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, 'Current MessageBox state: seq = %r, date = %s, map = %r',
self.seq, self.date.isoformat(), self.map)
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 the given entries.
#
# It also updates the next deadline time to reflect the new closest deadline.
def reset_deadlines(self, entries, deadline):
if not entries:
return
for entry in entries:
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 in entries:
# 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.
# Any entry will do, so the one from the last iteration is fine.
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_deadlines({channel_id}, get_running_loop().time() + (timeout or NO_UPDATES_TIMEOUT))
# 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, reason):
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')
if __debug__:
self._trace('Should get difference for %r because %s but cannot due to missing hash', entry, reason)
return
if __debug__:
self._trace('Marking %r as needing difference because %s', entry, reason)
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_deadlines({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
):
# 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 = []
date = getattr(updates, 'date', None)
seq = getattr(updates, 'seq', None)
seq_start = getattr(updates, 'seq_start', None)
users = getattr(updates, 'users', None) or []
chats = getattr(updates, 'chats', None) or []
if __debug__:
self._trace('Processing updates with seq = %r, seq_start = %r, date = %s: %s',
seq, seq_start, date.isoformat() if date else None, updates)
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, 'received updatesTooLong')
raise GapError
if seq is None:
seq = NO_SEQ
if seq_start is None:
seq_start = seq
# 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
if __debug__:
self._trace('Skipping updates as they should have already been handled')
return (users, chats)
elif self.seq + 1 < seq_start:
# Gap detected
self.try_begin_get_diff(ENTRY_ACCOUNT, 'detected gap')
raise GapError
# else apply
def _sort_gaps(update):
pts = PtsInfo.from_update(update)
return pts.pts - pts.pts_count if pts else 0
reset_deadlines = set() # temporary buffer
result.extend(filter(None, (
self.apply_pts_info(u, reset_deadlines=reset_deadlines)
# 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), so we sort them first.
for u in sorted(updates, key=_sort_gaps))))
self.reset_deadlines(reset_deadlines, next_updates_deadline())
if self.possible_gaps:
if __debug__:
self._trace('Trying to re-apply %r possible gaps', len(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_deadlines=None)
if update:
result.append(update)
if __debug__:
self._trace('Resolved gap with %r: %s', PtsInfo.from_update(update), 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)
if result and not self.possible_gaps:
# > If the updates were applied, local *Updates* state must be updated
# > with `seq` (unless it's 0) and `date` from the constructor.
if __debug__:
self._trace('Updating seq as all updates were applied')
if date != epoch():
self.date = date
if seq != NO_SEQ:
self.seq = seq
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_deadlines,
):
# 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, 'received updateChannelTooLong')
return None
pts = PtsInfo.from_update(update)
if not pts:
# No pts means that the update can be applied in any order.
if __debug__:
self._trace('No pts in update, so it can be applied in any order: %s', update)
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_deadlines:
reset_deadlines.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).
if __debug__:
self._trace('Skipping update with %r as its difference is being fetched', pts)
return None
if pts.entry in self.map:
local_pts = self.map[pts.entry].pts
if local_pts + pts.pts_count > pts.pts:
# Ignore
if __debug__:
self._trace('Skipping update since local pts %r > %r: %s', local_pts, pts, update)
return None
elif local_pts + pts.pts_count < pts.pts:
# Possible gap
# TODO store chats too?
if __debug__:
self._trace('Possible gap since local pts %r < %r: %s', local_pts, pts, update)
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
if __debug__:
self._trace('Applying update pts since local pts %r = %r: %s', local_pts, pts, update)
# 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=epoch(),
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=epoch(),
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.

View File

@ -1,195 +0,0 @@
from typing import Optional, Tuple
from enum import IntEnum
from ..tl.types import InputPeerUser, InputPeerChat, InputPeerChannel
import struct
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 {blob!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__})

View File

@ -19,7 +19,6 @@ from .messages import MessageMethods
from .chats import ChatMethods from .chats import ChatMethods
from .dialogs import DialogMethods from .dialogs import DialogMethods
from .downloads import DownloadMethods from .downloads import DownloadMethods
from .account import AccountMethods
from .auth import AuthMethods from .auth import AuthMethods
from .bots import BotMethods from .bots import BotMethods
from .telegramclient import TelegramClient from .telegramclient import TelegramClient

View File

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

View File

@ -1,54 +1,56 @@
import getpass import getpass
import hashlib
import inspect import inspect
import os import os
import sys import sys
import typing
import warnings
from .. import utils, helpers, errors, password as pwd_mod from .messageparse import MessageParseMethods
from ..tl import types, functions, custom from .users import UserMethods
from .._updates import SessionState from .. import utils, helpers, errors
from ..tl import types, functions
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
class AuthMethods: class AuthMethods(MessageParseMethods, UserMethods):
# region Public methods # region Public methods
def start( def start(
self: 'TelegramClient', self,
phone: typing.Union[typing.Callable[[], str], str] = lambda: input('Please enter your phone (or bot token): '), phone=lambda: input('Please enter your phone (or bot token): '),
password: typing.Union[typing.Callable[[], str], str] = lambda: getpass.getpass('Please enter your password: '), password=lambda: getpass.getpass('Please enter your password: '),
*, *,
bot_token: str = None, bot_token=None, force_sms=False, code_callback=None,
force_sms: bool = False, first_name='New User', last_name='', max_attempts=3):
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). Convenience method to interactively connect and sign in if required,
also taking into consideration that 2FA may be enabled in the account.
By default, this method will be interactive (asking for If the phone doesn't belong to an existing account (and will hence
user input if needed), and will handle 2FA if enabled too. `sign_up` for a new one), **you are agreeing to Telegram's
Terms of Service. This is required and your account
will be banned otherwise.** See https://telegram.org/tos
and https://core.telegram.org/api/terms.
Example usage:
>>> client = ...
>>> client.start(phone)
Please enter the code you received: 12345
Please enter your password: *******
(You are now logged in)
If the event loop is already running, this method returns a If the event loop is already running, this method returns a
coroutine that you should await on your own code; otherwise coroutine that you should on your own code; otherwise
the loop is ran until said coroutine completes. the loop is ran until said coroutine completes.
Arguments Args:
phone (`str` | `int` | `callable`): phone (`str` | `int` | `callable`):
The phone (or callable without arguments to get it) The phone (or callable without arguments to get it)
to which the code will be sent. If a bot-token-like to which the code will be sent. If a bot-token-like
string is given, it will be used as such instead. string is given, it will be used as such instead.
The argument may be a coroutine.
password (`str`, `callable`, optional): password (`callable`, optional):
The password for 2 Factor Authentication (2FA). The password for 2 Factor Authentication (2FA).
This is only required if it is enabled in your account. This is only required if it is enabled in your account.
The argument may be a coroutine.
bot_token (`str`): bot_token (`str`):
Bot Token obtained by `@BotFather <https://t.me/BotFather>`_ Bot Token obtained by `@BotFather <https://t.me/BotFather>`_
@ -62,7 +64,6 @@ class AuthMethods:
code_callback (`callable`, optional): code_callback (`callable`, optional):
A callable that will be used to retrieve the Telegram A callable that will be used to retrieve the Telegram
login code. Defaults to `input()`. login code. Defaults to `input()`.
The argument may be a coroutine.
first_name (`str`, optional): first_name (`str`, optional):
The first name to be used if signing up. This has no The first name to be used if signing up. This has no
@ -75,27 +76,9 @@ class AuthMethods:
How many times the code/password callback should be How many times the code/password callback should be
retried or switching between signing in and signing up. retried or switching between signing in and signing up.
Returns Returns:
This `TelegramClient`, so initialization This `TelegramClient`, so initialization
can be chained with ``.start()``. 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: if code_callback is None:
def code_callback(): def code_callback():
@ -124,50 +107,22 @@ class AuthMethods:
max_attempts=max_attempts max_attempts=max_attempts
) )
return ( return (
coro if self.loop.is_running() coro
else self.loop.run_until_complete(coro)
) )
async def _start( def _start(
self: 'TelegramClient', phone, password, bot_token, force_sms, self, phone, password, bot_token, force_sms,
code_callback, first_name, last_name, max_attempts): code_callback, first_name, last_name, max_attempts):
if not self.is_connected(): if not self.is_connected():
await self.connect() self.connect()
# 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; '
'if you were expecting a different user, check whether '
'you are accidentally reusing an existing session'
)
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; '
'if you were expecting a different user, check whether '
'you are accidentally reusing an existing session'
)
if self.is_user_authorized():
return self return self
if not bot_token: if not bot_token:
# Turn the callable into a valid phone number (or bot token) # Turn the callable into a valid phone number (or bot token)
while callable(phone): while callable(phone):
value = phone() value = phone()
if inspect.isawaitable(value):
value = await value
if ':' in value: if ':' in value:
# Bot tokens have 'user_id:access_hash' format # Bot tokens have 'user_id:access_hash' format
bot_token = value bot_token = value
@ -176,32 +131,31 @@ class AuthMethods:
phone = utils.parse_phone(value) or phone phone = utils.parse_phone(value) or phone
if bot_token: if bot_token:
await self.sign_in(bot_token=bot_token) self.sign_in(bot_token=bot_token)
return self return self
me = None me = None
attempts = 0 attempts = 0
two_step_detected = False two_step_detected = False
await self.send_code_request(phone, force_sms=force_sms) sent_code = self.send_code_request(phone, force_sms=force_sms)
sign_up = not sent_code.phone_registered
while attempts < max_attempts: while attempts < max_attempts:
try: try:
value = code_callback() if sign_up:
if inspect.isawaitable(value): me = self.sign_up(
value = await value code_callback(), first_name, last_name)
else:
# Since sign-in with no code works (it sends the code) # Raises SessionPasswordNeededError if 2FA enabled
# we must double-check that here. Else we'll assume we me = self.sign_in(phone, code=code_callback())
# 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 break
except errors.SessionPasswordNeededError: except errors.SessionPasswordNeededError:
two_step_detected = True two_step_detected = True
break break
except errors.PhoneNumberOccupiedError:
sign_up = False
except errors.PhoneNumberUnoccupiedError:
sign_up = True
except (errors.PhoneCodeEmptyError, except (errors.PhoneCodeEmptyError,
errors.PhoneCodeExpiredError, errors.PhoneCodeExpiredError,
errors.PhoneCodeHashEmptyError, errors.PhoneCodeHashEmptyError,
@ -225,68 +179,36 @@ class AuthMethods:
if callable(password): if callable(password):
for _ in range(max_attempts): for _ in range(max_attempts):
try: try:
value = password() me = self.sign_in(
if inspect.isawaitable(value): phone=phone, password=password())
value = await value
me = await self.sign_in(phone=phone, password=value)
break break
except errors.PasswordHashInvalidError: except errors.PasswordHashInvalidError:
print('Invalid password. Please try again', print('Invalid password. Please try again',
file=sys.stderr) file=sys.stderr)
else: else:
raise errors.PasswordHashInvalidError(request=None) raise errors.PasswordHashInvalidError()
else: else:
me = await self.sign_in(phone=phone, password=password) me = self.sign_in(phone=phone, password=password)
# We won't reach here if any step failed (exit by exception) # We won't reach here if any step failed (exit by exception)
signed, name = 'Signed in successfully as ', utils.get_display_name(me) signed, name = 'Signed in successfully as', utils.get_display_name(me)
tos = '; remember to not break the ToS or you will risk an account ban!'
try: try:
print(signed, name, tos, sep='') print(signed, name)
except UnicodeEncodeError: except UnicodeEncodeError:
# Some terminals don't support certain characters # Some terminals don't support certain characters
print(signed, name.encode('utf-8', errors='ignore') print(signed, name.encode('utf-8', errors='ignore')
.decode('ascii', errors='ignore'), tos, sep='') .decode('ascii', errors='ignore'))
return self return self
def _parse_phone_and_hash(self, phone, phone_hash): def sign_in(
self, phone=None, code=None, *, password=None,
bot_token=None, phone_code_hash=None):
""" """
Helper method to both parse and validate phone and its hash. Starts or completes the sign in process with the given phone number
""" or code that Telegram sent.
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) Args:
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`): phone (`str` | `int`):
The phone to send the code to if no code was provided, The phone to send the code to if no code was provided,
or to override the phone that was previously used with or to override the phone that was previously used with
@ -300,378 +222,264 @@ class AuthMethods:
password (`str`): password (`str`):
2FA password, should be used if a previous call raised 2FA password, should be used if a previous call raised
``SessionPasswordNeededError``. SessionPasswordNeededError.
bot_token (`str`): bot_token (`str`):
Used to sign in as a bot. Not all requests will be available. Used to sign in as a bot. Not all requests will be available.
This should be the hash the `@BotFather <https://t.me/BotFather>`_ This should be the hash the @BotFather gave you.
gave you.
phone_code_hash (`str`, optional): phone_code_hash (`str`):
The hash returned by `send_code_request`. This can be left as The hash returned by .send_code_request. This can be set to None
`None` to use the last hash known for the phone to be used. to use the last hash known.
Returns Returns:
The signed in user, or the information about The signed in user, or the information about
:meth:`send_code_request`. :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() me = self.get_me()
if me: if me:
return me return me
if phone and not code and not password: if phone and not code and not password:
return await self.send_code_request(phone) return self.send_code_request(phone)
elif code: elif code:
phone, phone_code_hash = \ phone = utils.parse_phone(phone) or self._phone
self._parse_phone_and_hash(phone, phone_code_hash) phone_code_hash = \
phone_code_hash or self._phone_code_hash.get(phone, None)
if not phone:
raise ValueError(
'Please make sure to call send_code_request first.'
)
if not phone_code_hash:
raise ValueError('You also need to provide a phone_code_hash.')
# May raise PhoneCodeEmptyError, PhoneCodeExpiredError, # May raise PhoneCodeEmptyError, PhoneCodeExpiredError,
# PhoneCodeHashEmptyError or PhoneCodeInvalidError. # PhoneCodeHashEmptyError or PhoneCodeInvalidError.
request = functions.auth.SignInRequest( result = self(functions.auth.SignInRequest(
phone, phone_code_hash, str(code) phone, phone_code_hash, str(code)))
)
elif password: elif password:
pwd = await self(functions.account.GetPasswordRequest()) salt = (self(
request = functions.auth.CheckPasswordRequest( functions.account.GetPasswordRequest())).current_salt
pwd_mod.compute_check(pwd, password) result = self(functions.auth.CheckPasswordRequest(
) helpers.get_password_hash(password, salt)
))
elif bot_token: elif bot_token:
request = functions.auth.ImportBotAuthorizationRequest( result = self(functions.auth.ImportBotAuthorizationRequest(
flags=0, bot_auth_token=bot_token, flags=0, bot_auth_token=bot_token,
api_id=self.api_id, api_hash=self.api_hash api_id=self.api_id, api_hash=self.api_hash
) ))
else: else:
raise ValueError( raise ValueError(
'You must provide a phone and a code the first time, ' 'You must provide a phone and a code the first time, '
'and a password only if an RPCError was raised before.' 'and a password only if an RPCError was raised before.'
) )
try: self._self_input_peer = utils.get_input_peer(
result = await self(request) result.user, allow_self=False
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 self._authorized = True
return result.user
state = await self(functions.updates.GetStateRequest()) def sign_up(self, code, first_name, last_name=''):
# the server may send an old qts in getState
difference = await self(functions.updates.GetDifferenceRequest(pts=state.pts, date=state.date, qts=state.qts))
if isinstance(difference, types.updates.Difference):
state = difference.state
elif isinstance(difference, types.updates.DifferenceSlice):
state = difference.intermediate_state
elif isinstance(difference, types.updates.DifferenceTooLong):
state.pts = difference.pts
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. Signs up to Telegram if you don't have an account yet.
You must call .send_code_request(phone) first.
Arguments **By using this method you're agreeing to Telegram's
Terms of Service. This is required and your account
will be banned otherwise.** See https://telegram.org/tos
and https://core.telegram.org/api/terms.
Args:
code (`str` | `int`):
The code sent by Telegram
first_name (`str`):
The first name to be used by the new account.
last_name (`str`, optional)
Optional last name.
Returns:
The new created :tl:`User`.
"""
me = self.get_me()
if me:
return me
if self._tos and self._tos.text:
if self.parse_mode:
t = self.parse_mode.unparse(self._tos.text, self._tos.entities)
else:
t = self._tos.text
sys.stderr.write("{}\n".format(t))
sys.stderr.flush()
result = self(functions.auth.SignUpRequest(
phone_number=self._phone,
phone_code_hash=self._phone_code_hash.get(self._phone, ''),
phone_code=str(code),
first_name=first_name,
last_name=last_name
))
if self._tos:
self(
functions.help.AcceptTermsOfServiceRequest(self._tos.id))
self._self_input_peer = utils.get_input_peer(
result.user, allow_self=False
)
self._authorized = True
return result.user
def send_code_request(self, phone, *, force_sms=False):
"""
Sends a code request to the specified phone number.
Args:
phone (`str` | `int`): phone (`str` | `int`):
The phone to which the code will be sent. The phone to which the code will be sent.
force_sms (`bool`, optional): force_sms (`bool`, optional):
Whether to force sending as SMS. This has been deprecated. Whether to force sending as SMS.
See `issue #4050 <https://github.com/LonamiWebs/Telethon/issues/4050>`_ for context.
Returns Returns:
An instance of :tl:`SentCode`. 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 = utils.parse_phone(phone) or self._phone
phone_hash = self._phone_code_hash.get(phone) phone_hash = self._phone_code_hash.get(phone)
if not phone_hash: if not phone_hash:
try: try:
result = await self(functions.auth.SendCodeRequest( result = self(functions.auth.SendCodeRequest(
phone, self.api_id, self.api_hash, types.CodeSettings())) phone, self.api_id, self.api_hash))
except errors.AuthRestartError: except errors.AuthRestartError:
if _retry_count > 2: return self.send_code_request(phone, force_sms=force_sms)
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 self._tos = result.terms_of_service
if isinstance(result, types.auth.SentCodeSuccess): self._phone_code_hash[phone] = phone_hash = result.phone_code_hash
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: else:
force_sms = True force_sms = True
self._phone = phone self._phone = phone
if force_sms: if force_sms:
try: result = self(
result = await self( functions.auth.ResendCodeRequest(phone, phone_hash))
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 self._phone_code_hash[phone] = result.phone_code_hash
return result return result
async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin: def log_out(self):
"""
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. 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.
Returns
`True` if the operation was successful.
Example
.. code-block:: python
# Note: you will need to login again!
await client.log_out()
""" """
try: try:
await self(functions.auth.LogOutRequest()) self(functions.auth.LogOutRequest())
except errors.RPCError: except errors.RPCError:
return False return False
self._mb_entity_cache.set_self_user(None, None, None) self.disconnect()
self.session.delete()
self._authorized = False self._authorized = False
await self.disconnect()
await utils.maybe_async(self.session.delete())
self.session = None
return True return True
async def edit_2fa( def edit_2fa(
self: 'TelegramClient', self, current_password=None, new_password=None,
current_password: str = None, *, hint='', email=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. Changes the 2FA settings of the logged in user, according to the
passed parameters. Take note of the parameter explanations.
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. Has no effect if both current and new password are omitted.
Arguments current_password (`str`, optional):
current_password (`str`, optional): The current password, to authorize changing to ``new_password``.
The current password, to authorize changing to ``new_password``. Must be set if changing existing 2FA settings.
Must be set if changing existing 2FA settings. Must **not** be set if 2FA is currently disabled.
Must **not** be set if 2FA is currently disabled. Passing this by itself will remove 2FA (if correct).
Passing this by itself will remove 2FA (if correct).
new_password (`str`, optional): new_password (`str`, optional):
The password to set as 2FA. The password to set as 2FA.
If 2FA was already enabled, ``current_password`` **must** be set. If 2FA was already enabled, ``current_password`` **must** be set.
Leaving this blank or `None` will remove the password. Leaving this blank or ``None`` will remove the password.
hint (`str`, optional): hint (`str`, optional):
Hint to be displayed by Telegram when it asks for 2FA. Hint to be displayed by Telegram when it asks for 2FA.
Leaving unspecified is highly discouraged. Leaving unspecified is highly discouraged.
Has no effect if ``new_password`` is not set. Has no effect if ``new_password`` is not set.
email (`str`, optional): email (`str`, optional):
Recovery and verification email. If present, you must also Recovery and verification email. Raises ``EmailUnconfirmedError``
set `email_code_callback`, else it raises ``ValueError``. if value differs from current one, and has no effect if
``new_password`` is not set.
email_code_callback (`callable`, optional): Returns:
If an email is provided, a callback that returns the code sent ``True`` if successful, ``False`` otherwise.
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: if new_password is None and current_password is None:
return False return False
if email and not callable(email_code_callback): pass_result = self(functions.account.GetPasswordRequest())
raise ValueError('email present without email_code_callback') if isinstance(
pass_result, types.account.NoPassword) and current_password:
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 current_password = None
if current_password: salt_random = os.urandom(8)
password = pwd_mod.compute_check(pwd, current_password) salt = pass_result.new_salt + salt_random
if not current_password:
current_password_hash = salt
else: else:
password = types.InputCheckPasswordEmpty() current_password = (
pass_result.current_salt
+ current_password.encode()
+ pass_result.current_salt
)
current_password_hash = hashlib.sha256(current_password).digest()
if new_password: if new_password: # Setting new password
new_password_hash = pwd_mod.compute_digest( new_password = salt + new_password.encode('utf-8') + salt
pwd.new_algo, new_password) new_password_hash = hashlib.sha256(new_password).digest()
else: new_settings = types.account.PasswordInputSettings(
new_password_hash = b'' new_salt=salt,
new_password_hash=new_password_hash,
try: hint=hint
await self(functions.account.UpdatePasswordSettingsRequest( )
password=password, if email: # If enabling 2FA or changing email
new_settings.email = email # TG counts empty string as None
return self(functions.account.UpdatePasswordSettingsRequest(
current_password_hash, new_settings=new_settings
))
else: # Removing existing password
return self(functions.account.UpdatePasswordSettingsRequest(
current_password_hash,
new_settings=types.account.PasswordInputSettings( new_settings=types.account.PasswordInputSettings(
new_algo=pwd.new_algo, new_salt=bytes(),
new_password_hash=new_password_hash, new_password_hash=bytes(),
hint=hint, 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 # endregion
# region with blocks # region with blocks
async def __aenter__(self): def __enter__(self):
return await self.start() return self.start()
async def __aexit__(self, *args): def __aenter__(self):
await self.disconnect() return self.start()
__enter__ = helpers._sync_enter def __exit__(self, *args):
__exit__ = helpers._sync_exit self.disconnect()
def __aexit__(self, *args):
self.disconnect()
# endregion # endregion

View File

@ -1,40 +1,23 @@
import typing from .users import UserMethods
from .. import hints
from ..tl import types, functions, custom from ..tl import types, functions, custom
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
class BotMethods(UserMethods):
class BotMethods: def inline_query(self, bot, query, *, offset=None, geo_point=None):
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``). Makes the given inline query to the specified bot
i.e. ``@vote My New Poll`` would be as follows:
Arguments >>> client = ...
>>> client.inline_query('vote', 'My New Poll')
Args:
bot (`entity`): bot (`entity`):
The bot entity to which the inline query should be made. The bot entity to which the inline query should be made.
query (`str`): query (`str`):
The query that should be made to the bot. 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): offset (`str`, optional):
The string offset to use for the bot. The string offset to use for the bot.
@ -42,31 +25,21 @@ class BotMethods:
The geo point location information to send to the bot The geo point location information to send to the bot
for localised results. Available under some bots. for localised results. Available under some bots.
Returns Returns:
A list of `custom.InlineResult A list of `custom.InlineResult
<telethon.tl.custom.inlineresult.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) bot = self.get_input_entity(bot)
if entity: result = self(functions.messages.GetInlineBotResultsRequest(
peer = await self.get_input_entity(entity)
else:
peer = types.InputPeerEmpty()
result = await self(functions.messages.GetInlineBotResultsRequest(
bot=bot, bot=bot,
peer=peer, peer=types.InputPeerEmpty(),
query=query, query=query,
offset=offset or '', offset=offset or '',
geo_point=geo_point geo_point=geo_point
)) ))
return custom.InlineResults(self, result, entity=peer if entity else None) # TODO Custom InlineResults(UserList) class with more information
return [
custom.InlineResult(self, x, query_id=result.query_id)
for x in result.results
]

View File

@ -1,101 +1,69 @@
import typing from .updates import UpdateMethods
from .. import utils, hints
from ..tl import types, custom from ..tl import types, custom
from .. import utils, events
class ButtonMethods: class ButtonMethods(UpdateMethods):
@staticmethod def build_reply_markup(self, buttons, inline_only=False):
def build_reply_markup(
buttons: 'typing.Optional[hints.MarkupLike]'
) -> 'typing.Optional[types.TypeReplyMarkup]':
""" """
Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for Builds a :tl`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for
the given buttons. the given buttons, or does nothing if either no buttons are
provided or the provided argument is already a reply markup.
Does nothing if either no buttons are provided or the provided This will add any event handlers defined in the
argument is already a reply markup. buttons and delete old ones not to call them twice,
so you should probably call this method manually for
You should consider using this method if you are going to reuse serious bots instead re-adding handlers every time you
the markup very often. Otherwise, it is not necessary. send a message. Magic can only go so far.
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.
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: if buttons is None:
return None return None
try: try:
if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: # crc32(b'ReplyMarkup'): if buttons.SUBCLASS_OF_ID == 0xe2e10ef2:
return buttons return buttons # crc32(b'ReplyMarkup'):
except AttributeError: except AttributeError:
pass pass
if not utils.is_list_like(buttons): if not utils.is_list_like(buttons):
buttons = [[buttons]] buttons = [[buttons]]
elif not buttons or not utils.is_list_like(buttons[0]): elif not utils.is_list_like(buttons[0]):
buttons = [buttons] buttons = [buttons]
is_inline = False is_inline = False
is_normal = False is_normal = False
resize = None
single_use = None
selective = None
persistent = None
placeholder = None
rows = [] rows = []
for row in buttons: for row in buttons:
current = [] current = []
for button in row: 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
if button.persistent is not None:
persistent = button.persistent
if button.placeholder is not None:
placeholder = button.placeholder
button = button.button
elif isinstance(button, custom.MessageButton):
button = button.button
inline = custom.Button._is_inline(button) inline = custom.Button._is_inline(button)
is_inline |= inline is_inline |= inline
is_normal |= not inline is_normal |= not inline
if isinstance(button, custom.Button):
if button.callback:
self.remove_event_handler(
button.callback, events.CallbackQuery)
if button.SUBCLASS_OF_ID == 0xbad74a3: # crc32(b'KeyboardButton') self.add_event_handler(
button.callback,
events.CallbackQuery(data=button.data)
)
button = button.button
if button.SUBCLASS_OF_ID == 0xbad74a3:
# 0xbad74a3 == crc32(b'KeyboardButton')
current.append(button) current.append(button)
if current: if current:
rows.append(types.KeyboardButtonRow(current)) rows.append(types.KeyboardButtonRow(current))
if is_inline and is_normal: 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') raise ValueError('You cannot mix inline with normal buttons')
elif is_inline: elif is_inline:
return types.ReplyInlineMarkup(rows) return types.ReplyInlineMarkup(rows)
return types.ReplyKeyboardMarkup( elif is_normal:
rows=rows, return types.ReplyKeyboardMarkup(rows)
resize=resize,
single_use=single_use,
selective=selective,
persistent=persistent,
placeholder=placeholder
)

File diff suppressed because it is too large Load Diff

View File

@ -1,172 +1,28 @@
import asyncio
import inspect
import itertools import itertools
import typing from collections import UserList
from .. import helpers, utils, hints, errors from .users import UserMethods
from ..requestiter import RequestIter from .. import utils
from ..tl import types, functions, custom from ..tl import types, functions, custom
_MAX_CHUNK_SIZE = 100
if typing.TYPE_CHECKING: class DialogMethods(UserMethods):
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 # region Public methods
def iter_dialogs( def iter_dialogs(
self: 'TelegramClient', self, limit=None, *, offset_date=None, offset_id=0,
limit: float = None, offset_peer=types.InputPeerEmpty(), ignore_migrated=False,
*, _total=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). Returns an iterator over the dialogs, yielding 'limit' at most.
Dialogs are the open "chats" or conversations with other people,
groups you have joined, or channels you are subscribed to.
The order is the same as the one seen in official applications Args:
(first pinned, them from those with the most recent message to
those with the oldest message).
Arguments
limit (`int` | `None`): limit (`int` | `None`):
How many dialogs to be retrieved as maximum. Can be set to How many dialogs to be retrieved as maximum. Can be set to
`None` to retrieve all dialogs. Note that this may take ``None`` to retrieve all dialogs. Note that this may take
whole minutes if you have hundreds of dialogs, as Telegram whole minutes if you have hundreds of dialogs, as Telegram
will tell the library to slow down through a will tell the library to slow down through a
``FloodWaitError``. ``FloodWaitError``.
@ -180,431 +36,122 @@ class DialogMethods:
offset_peer (:tl:`InputPeer`, optional): offset_peer (:tl:`InputPeer`, optional):
The peer to be used as an offset. 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): ignore_migrated (`bool`, optional):
Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel` Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel`
should be included or not. By default all the chats in your should be included or not. By default all the chats in your
dialogs are returned, but setting this to `True` will ignore dialogs are returned, but setting this to ``True`` will hide
(i.e. skip) them in the same way official applications do. them in the same way official applications do.
folder (`int`, optional): _total (`list`, optional):
The folder from which the dialogs should be retrieved. A single-item list to pass the total parameter by reference.
If left unspecified, all dialogs (including those from Yields:
folders) will be returned. Instances of `telethon.tl.custom.dialog.Dialog`.
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: limit = float('inf') if limit is None else int(limit)
folder = 1 if archived else 0 if limit == 0:
if not _total:
return
# Special case, get a single dialog and determine count
dialogs = self(functions.messages.GetDialogsRequest(
offset_date=offset_date,
offset_id=offset_id,
offset_peer=offset_peer,
limit=1,
hash=0
))
_total[0] = getattr(dialogs, 'count', len(dialogs.dialogs))
return
return _DialogsIter( seen = set()
self, req = functions.messages.GetDialogsRequest(
limit,
offset_date=offset_date, offset_date=offset_date,
offset_id=offset_id, offset_id=offset_id,
offset_peer=offset_peer, offset_peer=offset_peer,
ignore_pinned=ignore_pinned, limit=0,
ignore_migrated=ignore_migrated, hash=0
folder=folder
) )
while len(seen) < limit:
req.limit = min(limit - len(seen), 100)
r = self(req)
async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': if _total:
_total[0] = getattr(r, 'count', len(r.dialogs))
entities = {utils.get_peer_id(x): x
for x in itertools.chain(r.users, r.chats)}
messages = {}
for m in r.messages:
m._finish_init(self, entities, None)
messages[m.id] = m
# Happens when there are pinned dialogs
if len(r.dialogs) > limit:
r.dialogs = r.dialogs[:limit]
for d in r.dialogs:
peer_id = utils.get_peer_id(d.peer)
if peer_id not in seen:
seen.add(peer_id)
cd = custom.Dialog(self, d, entities, messages)
if cd.dialog.pts:
self._channel_pts[cd.id] = cd.dialog.pts
if not ignore_migrated or getattr(
cd.entity, 'migrated_to', None) is None:
yield (cd)
if len(r.dialogs) < req.limit\
or not isinstance(r, types.messages.DialogsSlice):
# Less than we requested means we reached the end, or
# we didn't get a DialogsSlice which means we got all.
break
req.offset_date = r.messages[-1].date
req.offset_peer = entities[utils.get_peer_id(r.dialogs[-1].peer)]
if req.offset_id == r.messages[-1].id:
# In some very rare cases this will get stuck in an infinite
# loop, where the offsets will get reused over and over. If
# the new offset is the same as the one before, break already.
break
req.offset_id = r.messages[-1].id
req.exclude_pinned = True
def get_dialogs(self, *args, **kwargs):
""" """
Same as `iter_dialogs()`, but returns a Same as :meth:`iter_dialogs`, but returns a list instead
`TotalList <telethon.helpers.TotalList>` instead. with an additional ``.total`` attribute on the list.
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() total = [0]
kwargs['_total'] = total
dialogs = UserList()
for x in self.iter_dialogs(*args, **kwargs):
dialogs.append(x)
dialogs.total = total[0]
return dialogs
get_dialogs.__signature__ = inspect.signature(iter_dialogs) def iter_drafts(self):
def iter_drafts(
self: 'TelegramClient',
entity: 'hints.EntitiesLike' = None
) -> _DraftsIter:
""" """
Iterator over draft messages. Iterator over all open draft messages.
The order is unspecified. Instances of `telethon.tl.custom.draft.Draft` are yielded.
You can call `telethon.tl.custom.draft.Draft.set_message`
Arguments to change the message or `telethon.tl.custom.draft.Draft.delete`
entity (`hints.EntitiesLike`, optional): among other things.
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): r = self(functions.messages.GetAllDraftsRequest())
entity = (entity,) for update in r.updates:
yield (custom.Draft._from_update(self, update))
# TODO Passing a limit here makes no sense def get_drafts(self):
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. Same as :meth:`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() result = []
if not entity or utils.is_list_like(entity): for x in self.iter_drafts():
return items result.append(x)
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 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 # endregion

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,28 @@
import itertools import itertools
import re import re
import typing
from .. import helpers, utils from .users import UserMethods
from ..tl import types from .. import utils
from ..tl import types, custom
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
class MessageParseMethods: class MessageParseMethods(UserMethods):
# region Public properties # region Public properties
@property @property
def parse_mode(self: 'TelegramClient'): def parse_mode(self):
""" """
This property is the default parse mode used when sending messages. This property is the default parse mode used when sending messages.
Defaults to `telethon.extensions.markdown`. It will always Defaults to `telethon.extensions.markdown`. It will always
be either `None` or an object with ``parse`` and ``unparse`` be either ``None`` or an object with ``parse`` and ``unparse``
methods. methods.
When setting a different value it should be one of: When setting a different value it should be one of:
* Object with ``parse`` and ``unparse`` methods. * Object with ``parse`` and ``unparse`` methods.
* A ``callable`` to act as the parse method. * A ``callable`` to act as the parse method.
* A `str` indicating the ``parse_mode``. For Markdown ``'md'`` * A ``str`` indicating the ``parse_mode``. For Markdown ``'md'``
or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'`` or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'``
may be used. may be used.
@ -37,27 +34,18 @@ class MessageParseMethods:
that ``assert text == unparse(*parse(text))``. that ``assert text == unparse(*parse(text))``.
See :tl:`MessageEntity` for allowed message entities. 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 return self._parse_mode
@parse_mode.setter @parse_mode.setter
def parse_mode(self: 'TelegramClient', mode: str): def parse_mode(self, mode):
self._parse_mode = utils.sanitize_parse_mode(mode) self._parse_mode = utils.sanitize_parse_mode(mode)
# endregion # endregion
# region Private methods # region Private methods
async def _replace_with_mention(self: 'TelegramClient', entities, i, user): def _replace_with_mention(self, entities, i, user):
""" """
Helper method to replace ``entities[i]`` to mention ``user``, Helper method to replace ``entities[i]`` to mention ``user``,
or do nothing if it can't be found. or do nothing if it can't be found.
@ -65,17 +53,16 @@ class MessageParseMethods:
try: try:
entities[i] = types.InputMessageEntityMentionName( entities[i] = types.InputMessageEntityMentionName(
entities[i].offset, entities[i].length, entities[i].offset, entities[i].length,
await self.get_input_entity(user) self.get_input_entity(user)
) )
return True
except (ValueError, TypeError): except (ValueError, TypeError):
return False pass
async def _parse_message_text(self: 'TelegramClient', message, parse_mode): def _parse_message_text(self, message, parse_mode):
""" """
Returns a (parsed message, entities) tuple depending on ``parse_mode``. Returns a (parsed message, entities) tuple depending on ``parse_mode``.
""" """
if parse_mode == (): if parse_mode == utils.Default:
parse_mode = self._parse_mode parse_mode = self._parse_mode
else: else:
parse_mode = utils.sanitize_parse_mode(parse_mode) parse_mode = utils.sanitize_parse_mode(parse_mode)
@ -83,42 +70,36 @@ class MessageParseMethods:
if not parse_mode: if not parse_mode:
return message, [] return message, []
original_message = message
message, msg_entities = parse_mode.parse(message) message, msg_entities = parse_mode.parse(message)
if original_message and not message and not msg_entities: for i, e in enumerate(msg_entities):
raise ValueError("Failed to parse message") if isinstance(e, types.MessageEntityTextUrl):
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) m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url)
if m: if m:
user = int(m.group(1)) if m.group(1) else e.url user = int(m.group(1)) if m.group(1) else e.url
is_mention = await self._replace_with_mention(msg_entities, i, user) self._replace_with_mention(msg_entities, i, user)
if not is_mention:
del msg_entities[i]
elif isinstance(e, (types.MessageEntityMentionName, elif isinstance(e, (types.MessageEntityMentionName,
types.InputMessageEntityMentionName)): types.InputMessageEntityMentionName)):
is_mention = await self._replace_with_mention(msg_entities, i, e.user_id) self._replace_with_mention(msg_entities, i, e.user_id)
if not is_mention:
del msg_entities[i]
return message, msg_entities return message, msg_entities
def _get_response_message(self: 'TelegramClient', request, result, input_chat): def _get_response_message(self, request, result, input_chat):
""" """
Extracts the response message known a request and Update result. Extracts the response message known a request and Update result.
The request may also be the ID of the message to match. 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.
""" """
# Telegram seems to send updateMessageID first, then updateNewMessage,
# however let's not rely on that just in case.
if isinstance(request, int):
msg_id = request
else:
msg_id = None
for update in result.updates:
if isinstance(update, types.UpdateMessageID):
if update.random_id == request.random_id:
msg_id = update.id
break
if isinstance(result, types.UpdateShort): if isinstance(result, types.UpdateShort):
updates = [result.update] updates = [result.update]
entities = {} entities = {}
@ -128,106 +109,31 @@ class MessageParseMethods:
for x in for x in
itertools.chain(result.users, result.chats)} itertools.chain(result.users, result.chats)}
else: else:
return None return
random_to_id = {} found = None
id_to_message = {}
for update in updates: for update in updates:
if isinstance(update, types.UpdateMessageID): if isinstance(update, (
random_to_id[update.random_id] = update.id
elif isinstance(update, (
types.UpdateNewChannelMessage, types.UpdateNewMessage)): types.UpdateNewChannelMessage, types.UpdateNewMessage)):
update.message._finish_init(self, entities, input_chat) if update.message.id == msg_id:
found = update.message
# Pinning a message with `updatePinnedMessage` seems to break
# 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) elif (isinstance(update, types.UpdateEditMessage)
and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL): and not isinstance(request.peer, types.InputPeerChannel)):
update.message._finish_init(self, entities, input_chat) if request.id == update.message.id:
found = update.message
# Live locations use `sendMedia` but Telegram responds with break
# `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) elif (isinstance(update, types.UpdateEditChannelMessage)
and utils.get_peer_id(request.peer) == and utils.get_peer_id(request.peer) ==
utils.get_peer_id(update.message.peer_id)): utils.get_peer_id(update.message.to_id)):
if request.id == update.message.id: if request.id == update.message.id:
update.message._finish_init(self, entities, input_chat) found = update.message
return update.message break
elif isinstance(update, types.UpdateNewScheduledMessage): if found:
update.message._finish_init(self, entities, input_chat) found._finish_init(self, entities, input_chat)
# Scheduled IDs may collide with normal IDs. However, for a return found
# 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 # endregion

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,13 @@
from . import ( from . import (
AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, BotMethods,
BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods, MessageMethods, ButtonMethods, UpdateMethods, UploadMethods,
MessageParseMethods, UserMethods, TelegramBaseClient MessageParseMethods, UserMethods
) )
class TelegramClient( class TelegramClient(
AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, BotMethods,
BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods,
MessageParseMethods, UserMethods, TelegramBaseClient MessageParseMethods, UserMethods
): ):
pass pass

View File

@ -1,125 +1,55 @@
import asyncio import concurrent.futures
import inspect
import itertools import itertools
import random
import sys
import time
import traceback
import typing
import logging import logging
import warnings import random
from collections import deque import time
import sqlite3
from .. import events, utils, errors from .users import UserMethods
from ..events.common import EventBuilder, EventCommon from .. import syncio, events, utils, errors
from ..tl import types, functions from ..tl import types, functions
from .._updates import GapError, PrematureEndReason
from ..helpers import get_running_loop __log__ = logging.getLogger(__name__)
from ..version import __version__
if typing.TYPE_CHECKING: class UpdateMethods(UserMethods):
from .telegramclient import TelegramClient
Callback = typing.Callable[[typing.Any], typing.Any]
class UpdateMethods:
# region Public methods # region Public methods
async def _run_until_disconnected(self: 'TelegramClient'): def _run_until_disconnected(self):
try: try:
# Make a high-level request to notify that we want updates self.disconnected.result()
await self(functions.updates.GetStateRequest())
result = await self.disconnected
if self._updates_error is not None:
raise self._updates_error
return result
except KeyboardInterrupt: 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() self.disconnect()
def on(self: 'TelegramClient', event: EventBuilder): def run_until_disconnected(self):
""" """
Decorator used to `add_event_handler` more conveniently. Runs the event loop until `disconnect` is called or if an error
while connecting/sending/receiving occurs in the background. In
the latter case, said error will ``raise`` 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 on your own code.
"""
return self._run_until_disconnected()
Arguments def on(self, event):
"""
Decorator helper method around `add_event_handler`. Example:
>>> from telethon import TelegramClient, events
>>> client = TelegramClient(...)
>>>
>>> @client.on(events.NewMessage)
... def handler(event):
... ...
...
>>>
Args:
event (`_EventBuilder` | `type`): event (`_EventBuilder` | `type`):
The event builder class or instance to be used, The event builder class or instance to be used,
for instance ``events.NewMessage``. 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): def decorator(f):
self.add_event_handler(f, event) self.add_event_handler(f, event)
@ -127,23 +57,14 @@ class UpdateMethods:
return decorator return decorator
def add_event_handler( def add_event_handler(self, callback, event=None):
self: 'TelegramClient',
callback: Callback,
event: EventBuilder = None):
""" """
Registers a new event handler callback. Registers the given callback to be called on the specified event.
The callback will be called when the specified event occurs. Args:
Arguments
callback (`callable`): callback (`callable`):
The callable function accepting one parameter to be used. 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): event (`_EventBuilder` | `type`, optional):
The event builder class or instance to be used, The event builder class or instance to be used,
for instance ``events.NewMessage``. for instance ``events.NewMessage``.
@ -151,55 +72,22 @@ class UpdateMethods:
If left unspecified, `telethon.events.raw.Raw` (the If left unspecified, `telethon.events.raw.Raw` (the
:tl:`Update` objects with no further processing) will :tl:`Update` objects with no further processing) will
be passed instead. 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): if isinstance(event, type):
event = event() event = event()
elif not event: elif not event:
event = events.Raw() event = events.Raw()
self._events_pending_resolve.append(event)
self._event_builders_count[type(event)] += 1
self._event_builders.append((event, callback)) self._event_builders.append((event, callback))
def remove_event_handler( def remove_event_handler(self, callback, event=None):
self: 'TelegramClient',
callback: Callback,
event: EventBuilder = None) -> int:
""" """
Inverse operation of `add_event_handler()`. Inverse operation of :meth:`add_event_handler`.
If no event is given, all events for this callback are removed. If no event is given, all events for this callback are removed.
Returns how many callbacks were 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 found = 0
if event and not isinstance(event, type): if event and not isinstance(event, type):
@ -210,493 +98,195 @@ class UpdateMethods:
i -= 1 i -= 1
ev, cb = self._event_builders[i] ev, cb = self._event_builders[i]
if cb == callback and (not event or isinstance(ev, event)): if cb == callback and (not event or isinstance(ev, event)):
type_ev = type(ev)
self._event_builders_count[type_ev] -= 1
if not self._event_builders_count[type_ev]:
del self._event_builders_count[type_ev]
del self._event_builders[i] del self._event_builders[i]
found += 1 found += 1
return found return found
def list_event_handlers(self: 'TelegramClient')\ def list_event_handlers(self):
-> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]':
""" """
Lists all registered event handlers. Lists all added event handlers, returning a list of pairs
consisting of (callback, event).
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] return [(callback, event) for event, callback in self._event_builders]
async def catch_up(self: 'TelegramClient'): def catch_up(self):
""" state = self.session.get_update_state(0)
"Catches up" on the missed updates while the client was offline. if not state or not state.pts:
You should call this method after registering the event handlers return
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. self.session.catching_up = True
try:
while True:
d = self(functions.updates.GetDifferenceRequest(
state.pts, state.date, state.qts))
if isinstance(d, types.updates.DifferenceEmpty):
state.date = d.date
state.seq = d.seq
break
elif isinstance(d, (types.updates.DifferenceSlice,
types.updates.Difference)):
if isinstance(d, types.updates.Difference):
state = d.state
elif d.intermediate_state.pts > state.pts:
state = d.intermediate_state
else:
# TODO Figure out why other applications can rely on
# using always the intermediate_state to eventually
# reach a DifferenceEmpty, but that leads to an
# infinite loop here (so check against old pts to stop)
break
Example self._handle_update(types.Updates(
.. code-block:: python users=d.users,
chats=d.chats,
await client.catch_up() date=state.date,
""" seq=state.seq,
await self._updates_queue.put(types.UpdatesTooLong()) updates=d.other_updates + [
types.UpdateNewMessage(m, 0, 0)
for m in d.new_messages
]
))
elif isinstance(d, types.updates.DifferenceTooLong):
break
finally:
self.session.set_update_state(0, state)
self.session.catching_up = False
# endregion # endregion
# region Private methods # region Private methods
async def _update_loop(self: 'TelegramClient'): def _handle_update(self, update):
# If the MessageBox is not empty, the account had to be logged-in to fill in its state. self.session.process_entities(update)
# This flag is used to propagate the "you got logged-out" error up (but getting logged-out if isinstance(update, (types.Updates, types.UpdatesCombined)):
# can only happen if it was once logged-in). entities = {utils.get_peer_id(x): x for x in
was_once_logged_in = self._authorized is True or not self._message_box.is_empty() itertools.chain(update.users, update.chats)}
for u in update.updates:
u._entities = entities
self._handle_update(u)
elif isinstance(update, types.UpdateShort):
self._handle_update(update.update)
else:
update._entities = getattr(update, '_entities', {})
if self._updates_queue is None:
syncio.create_task(self._dispatch_update, update)
else:
self._updates_queue.put_nowait(update)
if not self._dispatching_updates_queue.is_set():
self._dispatching_updates_queue.set()
syncio.create_task(self._dispatch_queue_updates)
self._updates_error = None need_diff = False
try: if hasattr(update, 'pts') and update.pts is not None:
if self._catch_up: if self._state.pts and (update.pts - self._state.pts) > 1:
# User wants to catch up as soon as the client is up and running, need_diff = True
# so this is the best place to do it. self._state.pts = update.pts
await self.catch_up() if hasattr(update, 'date'):
self._state.date = update.date
if hasattr(update, 'seq'):
self._state.seq = update.seq
updates_to_dispatch = deque() # TODO make use of need_diff
while self.is_connected(): def _update_loop(self):
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
)
await 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.TimedOutError,
errors.FloodWaitError,
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, sqlite3.OperationalError) 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')
_preprocess_updates = await utils.maybe_async(self._preprocess_updates(updates, users, chats))
updates_to_dispatch.extend(_preprocess_updates)
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, sqlite3.OperationalError) 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.TimedOutError,
errors.FloodWaitError,
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)
_preprocess_updates = await utils.maybe_async(self._preprocess_updates(updates, users, chats))
updates_to_dispatch.extend(_preprocess_updates)
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
_preprocess_updates = await utils.maybe_async(self._preprocess_updates(processed, users, chats))
updates_to_dispatch.extend(_preprocess_updates)
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()
async def _preprocess_updates(self, updates, users, chats):
self._mb_entity_cache.extend(users, chats)
await utils.maybe_async(self.session.process_entities(types.contacts.ResolvedPeer(None, 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" # Pings' ID don't really need to be secure, just "random"
rnd = lambda: random.randrange(-2**63, 2**63) rnd = lambda: random.randrange(-2**63, 2**63)
while self.is_connected(): while self.is_connected():
try: try:
await asyncio.wait_for( next(concurrent.futures.as_completed(
self.disconnected, timeout=60 [self.disconnected], timeout=60))
)
continue # We actually just want to act upon timeout continue # We actually just want to act upon timeout
except asyncio.TimeoutError: except concurrent.futures.TimeoutError:
pass pass
except asyncio.CancelledError: except Exception as e:
return
except Exception:
continue # Any disconnected exception should be ignored 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. # We also don't really care about their result.
# Just send them periodically. # Just send them periodically.
try: self._sender.send(functions.PingRequest(rnd()))
self._sender._keepalive_ping(rnd())
except (ConnectionError, asyncio.CancelledError):
return
# Entities and cached files are not saved when they are # Entities and cached files are not saved when they are
# inserted because this is a rather expensive operation # inserted because this is a rather expensive operation
# (default's sqlite3 takes ~0.1s to commit changes). Do # (default's sqlite3 takes ~0.1s to commit changes). Do
# it every minute instead. No-op if there's nothing new. # it every minute instead. No-op if there's nothing new.
await self._save_states_and_entities() self.session.save()
await utils.maybe_async(self.session.save()) # We need to send some content-related request at least hourly
# for Telegram to keep delivering updates, otherwise they will
async def _dispatch_update(self: 'TelegramClient', update): # just stop even if we're connected. Do so every 30 minutes.
# 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 # TODO Call getDifference instead since it's more relevant
# fine, we will just retry next time anyway. if time.time() - self._last_request > 30 * 60:
try: if not self.is_user_authorized():
await self.get_me(input_peer=True) # What can be the user doing for so
except OSError: # long without being logged in...?
pass # might not have connection continue
built = EventBuilderDict(self, update, others) self(functions.updates.GetStateRequest())
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] def _dispatch_queue_updates(self):
if ev: while not self._updates_queue.empty():
conv._on_edit(ev) self._dispatch_update(self._updates_queue.get_nowait())
ev = built[events.MessageRead] self._dispatching_updates_queue.clear()
if ev:
conv._on_read(ev)
if conv._custom: def _dispatch_update(self, update):
await conv._check_custom(built) if self._events_pending_resolve:
if self._event_resolve_lock.locked():
with self._event_resolve_lock:
pass
else:
with self._event_resolve_lock:
for event in self._events_pending_resolve:
event.resolve(self)
self._events_pending_resolve.clear()
# TODO We can improve this further
# If we had a way to get all event builders for
# a type instead looping over them all always.
built = {builder: builder.build(update)
for builder in self._event_builders_count}
for builder, callback in self._event_builders: for builder, callback in self._event_builders:
event = built[type(builder)] event = built[type(builder)]
if not event: if not event or not builder.filter(event):
continue continue
if not builder.resolved: if hasattr(event, '_set_client'):
await builder.resolve(self) event._set_client(self)
else:
filter = builder.filter(event) event._client = self
if inspect.isawaitable(filter):
filter = await filter
if not filter:
continue
event.original_update = update
try: try:
await callback(event) 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: except events.StopPropagation:
name = getattr(callback, '__name__', repr(callback)) name = getattr(callback, '__name__', repr(callback))
self._log[__name__].debug( __log__.debug(
'Event handler "%s" stopped chain of propagation ' 'Event handler "%s" stopped chain of propagation '
'for event %s.', name, type(event).__name__ 'for event %s.', name, type(event).__name__
) )
break break
except Exception as e: except Exception:
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)) name = getattr(callback, '__name__', repr(callback))
self._log[__name__].debug( __log__.exception('Unhandled exception on %s', name)
'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'): def _handle_auto_reconnect(self):
# TODO Catch-up # Upon reconnection, we want to send getState
# For now we make a high-level request to let Telegram # for Telegram to keep sending us updates.
# know we are still interested in receiving more updates.
try: try:
await self.get_me() __log__.info('Asking for the current state after reconnect...')
except Exception as e: state = self(functions.updates.GetStateRequest())
self._log[__name__].warning('Error executing high-level request ' __log__.info('Got new state! %s', state)
'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: except errors.RPCError as e:
self._log[__name__].warning('Failed to get missed updates after ' __log__.info('Failed to get current state: %r', e)
'reconnect: %r', e)
except Exception:
self._log[__name__].exception(
'Unhandled exception while getting update difference after reconnect')
# endregion # 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

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +1,22 @@
import asyncio import asyncio
import datetime
import itertools import itertools
import logging
import time import time
import typing
from .. import errors, helpers, utils, hints from .telegrambaseclient import TelegramBaseClient
from ..errors import MultiError, RPCError from .. import errors, utils
from ..helpers import retry_range from ..tl import TLObject, TLRequest, types, functions
from ..tl import TLRequest, types, functions
_NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!') __log__ = logging.getLogger(__name__)
_NOT_A_REQUEST = 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): class UserMethods(TelegramBaseClient):
return ( def __call__(self, request, ordered=False):
'Sleeping%s for %ds (%s) on %s flood wait', for r in (request if utils.is_list_like(request) else (request,)):
' 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 = list(request) if utils.is_list_like(request) else [request]
request = list(request) if utils.is_list_like(request) else request
for i, r in enumerate(requests):
if not isinstance(r, TLRequest): if not isinstance(r, TLRequest):
raise _NOT_A_REQUEST() raise _NOT_A_REQUEST
await r.resolve(self, utils) r.resolve(self, utils)
# Avoid making the request if it's already in a flood wait # Avoid making the request if it's already in a flood wait
if r.CONSTRUCTOR_ID in self._flood_waited_requests: if r.CONSTRUCTOR_ID in self._flood_waited_requests:
@ -49,238 +24,133 @@ class UserMethods:
diff = round(due - time.time()) diff = round(due - time.time())
if diff <= 3: # Flood waits below 3 seconds are "ignored" if diff <= 3: # Flood waits below 3 seconds are "ignored"
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
elif diff <= flood_sleep_threshold: elif diff <= self.flood_sleep_threshold:
self._log[__name__].info(*_fmt_flood(diff, r, early=True)) __log__.info('Sleeping early for %ds on flood wait', diff)
await asyncio.sleep(diff) time.sleep(diff)
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
else: else:
raise errors.FloodWaitError(request=r, capture=diff) raise errors.FloodWaitError(capture=diff)
if self._no_updates:
if utils.is_list_like(request):
request[i] = functions.InvokeWithoutUpdatesRequest(r)
else:
# This should only run once as requests should be a list of 1 item
request = functions.InvokeWithoutUpdatesRequest(r)
request_index = 0 request_index = 0
last_error = None
self._last_request = time.time() self._last_request = time.time()
for _ in range(self._request_retries):
for attempt in retry_range(self._request_retries):
try: try:
future = sender.send(request, ordered=ordered) future = self._sender.send(request, ordered=ordered)
if isinstance(future, list): if isinstance(future, list):
results = [] results = []
exceptions = []
for f in future: for f in future:
try: result = f.result()
result = await f self.session.process_entities(result)
except RPCError as e:
exceptions.append(e)
results.append(None)
continue
await utils.maybe_async(self.session.process_entities(result))
exceptions.append(None)
results.append(result) results.append(result)
request_index += 1 request_index += 1
if any(x is not None for x in exceptions): return results
raise MultiError(exceptions, results, requests)
else:
return results
else: else:
result = await future result = future.result()
await utils.maybe_async(self.session.process_entities(result)) self.session.process_entities(result)
return result return result
except (errors.ServerError, errors.RpcCallFailError, except (errors.ServerError, errors.RpcCallFailError) as e:
errors.RpcMcgetFailError, errors.InterdcCallErrorError, __log__.warning('Telegram is having internal issues %s: %s',
errors.TimedOutError, e.__class__.__name__, e)
errors.InterdcCallRichErrorError) as e: except (errors.FloodWaitError, errors.FloodTestPhoneWaitError) 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.FloodPremiumWaitError,
errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e:
last_error = e
if utils.is_list_like(request): if utils.is_list_like(request):
request = request[request_index] request = request[request_index]
# SLOW_MODE_WAIT is chat-specific, not request-specific self._flood_waited_requests\
if not isinstance(e, errors.SlowModeWaitError): [request.CONSTRUCTOR_ID] = time.time() + e.seconds
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: if e.seconds <= self.flood_sleep_threshold:
self._log[__name__].info(*_fmt_flood(e.seconds, request)) __log__.info('Sleeping for %ds on flood wait', e.seconds)
await asyncio.sleep(e.seconds) time.sleep(e.seconds)
else: else:
raise raise
except (errors.PhoneMigrateError, errors.NetworkMigrateError, except (errors.PhoneMigrateError, errors.NetworkMigrateError,
errors.UserMigrateError) as e: errors.UserMigrateError) as e:
last_error = e __log__.info('Phone migrated to %d', e.new_dc)
self._log[__name__].info('Phone migrated to %d', e.new_dc)
should_raise = isinstance(e, ( should_raise = isinstance(e, (
errors.PhoneMigrateError, errors.NetworkMigrateError errors.PhoneMigrateError, errors.NetworkMigrateError
)) ))
if should_raise and await self.is_user_authorized(): if should_raise and self.is_user_authorized():
raise raise
await self._switch_dc(e.new_dc) self._switch_dc(e.new_dc)
if self._raise_last_call_error and last_error is not None: raise ValueError('Number of retries reached 0')
raise last_error
raise ValueError('Request was unsuccessful {} time(s)'
.format(attempt))
# region Public methods # region Public methods
async def get_me(self: 'TelegramClient', input_peer: bool = False) \ def get_me(self, input_peer=False):
-> 'typing.Union[types.User, types.InputPeerUser]':
""" """
Gets "me", the current :tl:`User` who is logged in. Gets "me" (the self user) which is currently authenticated,
or None if the request fails (hence, not authenticated).
If the user has not logged in yet, this method returns `None`. Args:
Arguments
input_peer (`bool`, optional): input_peer (`bool`, optional):
Whether to return the :tl:`InputPeerUser` version or the normal Whether to return the :tl:`InputPeerUser` version or the normal
:tl:`User`. This can be useful if you just need to know the ID :tl:`User`. This can be useful if you just need to know the ID
of yourself. of yourself.
Returns Returns:
Your own :tl:`User`. 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: if input_peer and self._self_input_peer:
return self._mb_entity_cache.get(self._mb_entity_cache.self_id)._as_input_peer() return self._self_input_peer
try: try:
me = (await self( me = (self(
functions.users.GetUsersRequest([types.InputUserSelf()])))[0] functions.users.GetUsersRequest([types.InputUserSelf()])))[0]
if not self._mb_entity_cache.self_id: if not self._self_input_peer:
self._mb_entity_cache.set_self_user(me.id, me.bot, me.access_hash) self._self_input_peer = utils.get_input_peer(
me, allow_self=False
)
return utils.get_input_peer(me, allow_self=False) if input_peer else me return self._self_input_peer if input_peer else me
except errors.UnauthorizedError: except errors.UnauthorizedError:
return None return None
@property def is_user_authorized(self):
def _self_id(self: 'TelegramClient') -> typing.Optional[int]:
""" """
Returns the ID of the logged-in user, if known. Returns ``True`` if the user is authorized.
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 if self._self_input_peer is not None or self._state.pts != -1:
return True
async def is_bot(self: 'TelegramClient') -> bool: try:
""" self._state = self(functions.updates.GetStateRequest())
Return `True` if the signed-in user is a bot, `False` otherwise. return True
except errors.RPCError:
return False
Example def get_entity(self, entity):
.. 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') -> typing.Union['hints.Entity', typing.List['hints.Entity']]:
""" """
Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat` Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat`
or :tl:`Channel`. You can also pass a list or iterable of entities, or :tl:`Channel`. You can also pass a list or iterable of entities,
and they will be efficiently fetched from the network. and they will be efficiently fetched from the network.
Arguments entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): If an username is given, **the username will be resolved** making
If a username is given, **the username will be resolved** making an API call every time. Resolving usernames is an expensive
an API call every time. Resolving usernames is an expensive operation and will start hitting flood waits around 50 usernames
operation and will start hitting flood waits around 50 usernames in a short period of time.
in a short period of time.
If you want to get the entity for a *cached* username, you should If you want to get the entity for a *cached* username, you should
first `get_input_entity(username) <get_input_entity>` which will first `get_input_entity(username) <get_input_entity>` which will
use the cache), and then use `get_entity` with the result of the use the cache), and then use `get_entity` with the result of the
previous call. previous call.
Similar limits apply to invite links, and you should use their Similar limits apply to invite links, and you should use their
ID instead. ID instead.
Using phone numbers (from people in your contact list), exact Using phone numbers, exact names, integer IDs or :tl:`Peer`
names, integer IDs or :tl:`Peer` rely on a `get_input_entity` rely on a `get_input_entity` first, which in turn needs the
first, which in turn needs the entity to be in cache, unless entity to be in cache, unless a :tl:`InputPeer` was passed.
a :tl:`InputPeer` was passed.
Unsupported types will raise ``TypeError``. Unsupported types will raise ``TypeError``.
If the entity can't be found, ``ValueError`` will be raised. If the entity can't be found, ``ValueError`` will be raised.
Returns Returns:
:tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the
input entity. A list will be returned if more than one was given. 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) single = not utils.is_list_like(entity)
if single: if single:
@ -295,41 +165,31 @@ class UserMethods:
if isinstance(x, str): if isinstance(x, str):
inputs.append(x) inputs.append(x)
else: else:
inputs.append(await self.get_input_entity(x)) inputs.append(self.get_input_entity(x))
lists = { users = [x for x in inputs
helpers._EntityType.USER: [], if isinstance(x, (types.InputPeerUser, types.InputPeerSelf))]
helpers._EntityType.CHAT: [], chats = [x.chat_id for x in inputs
helpers._EntityType.CHANNEL: [], if isinstance(x, types.InputPeerChat)]
} channels = [x for x in inputs
for x in inputs: if isinstance(x, types.InputPeerChannel)]
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: if users:
# GetUsersRequest has a limit of 200 per call # GetUsersRequest has a limit of 200 per call
tmp = [] tmp = []
while users: while users:
curr, users = users[:200], users[200:] curr, users = users[:200], users[200:]
tmp.extend(await self(functions.users.GetUsersRequest(curr))) tmp.extend(self(functions.users.GetUsersRequest(curr)))
users = tmp users = tmp
if chats: # TODO Handle chats slice? if chats: # TODO Handle chats slice?
chats = (await self( chats = (self(
functions.messages.GetChatsRequest([x.chat_id for x in chats]))).chats functions.messages.GetChatsRequest(chats))).chats
if channels: if channels:
channels = (await self( channels = (self(
functions.channels.GetChannelsRequest(channels))).chats functions.channels.GetChannelsRequest(channels))).chats
# Merge users, chats and channels into a single dictionary # Merge users, chats and channels into a single dictionary
id_entity = { id_entity = {
# `get_input_entity` might've guessed the type from a non-marked ID, utils.get_peer_id(x): x
# 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) for x in itertools.chain(users, chats, channels)
} }
@ -340,9 +200,9 @@ class UserMethods:
result = [] result = []
for x in inputs: for x in inputs:
if isinstance(x, str): if isinstance(x, str):
result.append(await self._get_entity_from_string(x)) result.append(self._get_entity_from_string(x))
elif not isinstance(x, types.InputPeerSelf): elif not isinstance(x, types.InputPeerSelf):
result.append(id_entity[utils.get_peer_id(x, add_mark=False)]) result.append(id_entity[utils.get_peer_id(x)])
else: else:
result.append(next( result.append(next(
u for u in id_entity.values() u for u in id_entity.values()
@ -351,168 +211,112 @@ class UserMethods:
return result[0] if single else result return result[0] if single else result
async def get_input_entity( def get_input_entity(self, peer):
self: 'TelegramClient',
peer: 'hints.EntityLike') -> 'types.TypeInputPeer':
""" """
Turns the given entity into its input entity version. Turns the given peer 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:
Most requests use this kind of :tl:`InputPeer`, so this is the most >>> from telethon import TelegramClient
suitable call to make for those cases. **Generally you should let the >>> client = TelegramClient(...)
library do its job** and don't worry about getting the input entity >>> # If you're going to use "username" often in your code
first, but if you're going to use an entity often, consider making the >>> # (make a lot of calls), consider getting its input entity
call: >>> # once, and then using the "user" everywhere instead.
>>> user = client.get_input_entity('username')
>>> # The same applies to IDs, chats or channels.
>>> chat = client.get_input_entity(-123456789)
Arguments entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): If an username is given, **the library will use the cache**. This
If a username or invite link is given, **the library will means that it's possible to be using an username that *changed*.
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 If the username is not found in the cache, it will be fetched.
the cache, it will be fetched. The same rules apply to phone The same rules apply to phone numbers (``'+34 123456789'``).
numbers (``'+34 123456789'``) from people in your contact list.
If an exact name is given, it must be in the cache too. This 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 is not reliable as different people can share the same name
and which entity is returned is arbitrary, and should be used and which entity is returned is arbitrary, and should be used
only for quick tests. only for quick tests.
If a positive integer ID is given, the entity will be searched If a positive integer ID is given, the entity will be searched
in cached users, chats or channels, without making any call. in cached users, chats or channels, without making any call.
If a negative integer ID is given, the entity will be searched If a negative integer ID is given, the entity will be searched
exactly as either a chat (prefixed with ``-``) or as a channel exactly as either a chat (prefixed with ``-``) or as a channel
(prefixed with ``-100``). (prefixed with ``-100``).
If a :tl:`Peer` is given, it will be searched exactly in the If a :tl:`Peer` is given, it will be searched exactly in the
cache as either a user, chat or channel. cache as either an user, chat or channel.
If the given object can be turned into an input entity directly, If the given object can be turned into an input entity directly,
said operation will be done. said operation will be done.
Unsupported types will raise ``TypeError``. Invite links make an API call **always** and are expensive.
You should use the chat ID instead.
If the entity can't be found, ``ValueError`` will be raised. Unsupported types will raise ``TypeError``.
Returns If the entity can't be found, ``ValueError`` will be raised.
Returns:
:tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel` :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`
or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``. or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``.
If you need to get the ID of yourself, you should use If you need to get the ID of yourself, you should use
`get_me` with ``input_peer=True``) instead. `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'): if peer in ('me', 'self'):
return types.InputPeerSelf() return types.InputPeerSelf()
# No InputPeer, cached peer, or known string. Fetch from disk cache
try: try:
input_entity = await utils.maybe_async(self.session.get_input_entity(peer)) # First try to get the entity from cache, otherwise figure it out
return input_entity return self.session.get_input_entity(peer)
except ValueError: except ValueError:
pass pass
# Only network left to try
if isinstance(peer, str): if isinstance(peer, str):
return utils.get_input_peer( return utils.get_input_peer(
await self._get_entity_from_string(peer)) self._get_entity_from_string(peer))
# If we're a bot and the user has messaged us privately users.getUsers if not isinstance(peer, int) and (not isinstance(peer, TLObject)
# will work with access_hash = 0. Similar for channels.getChannels. or peer.SUBCLASS_OF_ID != 0x2d45687):
# If we're not a bot but the user is in our contacts, it seems to work # Try casting the object into an input peer. Might TypeError.
# regardless. These are the only two special-cased requests. # Don't do it if a not-found ID was given (instead ValueError).
peer = utils.get_peer(peer) # Also ignore Peer (0x2d45687 == crc32(b'Peer'))'s, lacking hash.
if isinstance(peer, types.PeerUser): return utils.get_input_peer(peer)
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( raise ValueError(
'Could not find the input entity for {} ({}). Please read https://' 'Could not find the input entity for "{}". Please read https://'
'docs.telethon.dev/en/stable/concepts/entities.html to' 'telethon.readthedocs.io/en/latest/extra/basic/entities.html to'
' find out more details.' ' find out more details.'
.format(peer, type(peer).__name__) .format(peer)
) )
async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'): def get_peer_id(self, peer, add_mark=True):
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. Gets the ID for the given peer, which may be anything entity-like.
This method needs to be ``async`` because `peer` supports usernames, This method needs to be ``async`` because `peer` supports usernames,
invite-links, phone numbers (from people in your contact list), etc. invite-links, phone numbers, etc.
If ``add_mark is False``, then a positive ID will be returned If ``add_mark is False``, then a positive ID will be returned
instead. By default, bot-API style IDs (signed) are 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): if isinstance(peer, int):
return utils.get_peer_id(peer, add_mark=add_mark) return utils.get_peer_id(peer, add_mark=add_mark)
try: try:
if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): if peer.SUBCLASS_OF_ID in (0x2d45687, 0xc91c90b6):
# 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer' # 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer'
peer = await self.get_input_entity(peer) return utils.get_peer_id(peer)
except AttributeError: except AttributeError:
peer = await self.get_input_entity(peer) pass
peer = self.get_input_entity(peer)
if isinstance(peer, types.InputPeerSelf): if isinstance(peer, types.InputPeerSelf):
peer = await self.get_me(input_peer=True) peer = self.get_me(input_peer=True)
return utils.get_peer_id(peer, add_mark=add_mark) return utils.get_peer_id(peer, add_mark=add_mark)
@ -520,10 +324,10 @@ class UserMethods:
# region Private methods # region Private methods
async def _get_entity_from_string(self: 'TelegramClient', string): def _get_entity_from_string(self, string):
""" """
Gets a full entity from the given string, which may be a phone or Gets a full entity from the given string, which may be a phone or
a username, and processes all the found entities on the session. an username, and processes all the found entities on the session.
The string may also be a user link, or a channel/chat invite link. 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 This method has the side effect of adding the found users to the
@ -534,20 +338,14 @@ class UserMethods:
""" """
phone = utils.parse_phone(string) phone = utils.parse_phone(string)
if phone: if phone:
try: for user in (self(
for user in (await self( functions.contacts.GetContactsRequest(0))).users:
functions.contacts.GetContactsRequest(0))).users: if user.phone == phone:
if user.phone == phone: return user
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: else:
username, is_join_chat = utils.parse_username(string) username, is_join_chat = utils.parse_username(string)
if is_join_chat: if is_join_chat:
invite = await self( invite = self(
functions.messages.CheckChatInviteRequest(username)) functions.messages.CheckChatInviteRequest(username))
if isinstance(invite, types.ChatInvite): if isinstance(invite, types.ChatInvite):
@ -558,25 +356,24 @@ class UserMethods:
elif isinstance(invite, types.ChatInviteAlready): elif isinstance(invite, types.ChatInviteAlready):
return invite.chat return invite.chat
elif username: elif username:
if username in ('me', 'self'):
return self.get_me()
try: try:
result = await self( result = self(
functions.contacts.ResolveUsernameRequest(username)) functions.contacts.ResolveUsernameRequest(username))
except errors.UsernameNotOccupiedError as e: except errors.UsernameNotOccupiedError as e:
raise ValueError('No user has "{}" as username' raise ValueError('No user has "{}" as username'
.format(username)) from e .format(username)) from e
try: for entity in itertools.chain(result.users, result.chats):
pid = utils.get_peer_id(result.peer, add_mark=False) if getattr(entity, 'username', None) or '' \
if isinstance(result.peer, types.PeerUser): .lower() == username:
return next(x for x in result.users if x.id == pid) return entity
else:
return next(x for x in result.chats if x.id == pid)
except StopIteration:
pass
try: try:
# Nobody with this username, maybe it's an exact name/title # Nobody with this username, maybe it's an exact name/title
input_entity = await utils.maybe_async(self.session.get_input_entity(string)) return self.get_entity(
return await self.get_entity(input_entity) self.session.get_input_entity(string))
except ValueError: except ValueError:
pass pass
@ -584,24 +381,7 @@ class UserMethods:
'Cannot find any entity corresponding to "{}"'.format(string) 'Cannot find any entity corresponding to "{}"'.format(string)
) )
async def _get_input_dialog(self: 'TelegramClient', dialog): def _get_input_notify(self, notify):
"""
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 Returns a :tl:`InputNotifyPeer`. This is a bit tricky because
it may or not need access to the client to convert what's given it may or not need access to the client to convert what's given
@ -610,11 +390,9 @@ class UserMethods:
try: try:
if notify.SUBCLASS_OF_ID == 0x58981615: if notify.SUBCLASS_OF_ID == 0x58981615:
if isinstance(notify, types.InputNotifyPeer): if isinstance(notify, types.InputNotifyPeer):
notify.peer = await self.get_input_entity(notify.peer) notify.peer = self.get_input_entity(notify.peer)
return notify return notify
except AttributeError: except AttributeError:
pass return types.InputNotifyPeer(self.get_input_entity(notify))
return types.InputNotifyPeer(await self.get_input_entity(notify))
# endregion # endregion

View File

@ -20,28 +20,11 @@ class AuthKey:
""" """
self.key = data self.key = data
@property with BinaryReader(sha1(self.key).digest()) as reader:
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) self.aux_hash = reader.read_long(signed=False)
reader.read(4) reader.read(4)
self.key_id = reader.read_long(signed=False) 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): def calc_new_nonce_hash(self, new_nonce, number):
""" """
Calculates the new nonce hash based on the current attributes. Calculates the new nonce hash based on the current attributes.
@ -56,8 +39,5 @@ class AuthKey:
# Calculates the message key from the given data # Calculates the message key from the given data
return int.from_bytes(sha1(data).digest()[4:20], 'little', signed=True) return int.from_bytes(sha1(data).digest()[4:20], 'little', signed=True)
def __bool__(self):
return bool(self._key)
def __eq__(self, other): def __eq__(self, other):
return isinstance(other, type(self)) and other.key == self._key return isinstance(other, type(self)) and other.key == self.key

View File

@ -30,7 +30,7 @@ class CdnDecrypter:
self.cdn_file_hashes = cdn_file_hashes self.cdn_file_hashes = cdn_file_hashes
@staticmethod @staticmethod
async def prepare_decrypter(client, cdn_client, cdn_redirect): def prepare_decrypter(client, cdn_client, cdn_redirect):
""" """
Prepares a new CDN decrypter. Prepares a new CDN decrypter.
@ -52,14 +52,14 @@ class CdnDecrypter:
cdn_aes, cdn_redirect.cdn_file_hashes cdn_aes, cdn_redirect.cdn_file_hashes
) )
cdn_file = await cdn_client(GetCdnFileRequest( cdn_file = cdn_client(GetCdnFileRequest(
file_token=cdn_redirect.file_token, file_token=cdn_redirect.file_token,
offset=cdn_redirect.cdn_file_hashes[0].offset, offset=cdn_redirect.cdn_file_hashes[0].offset,
limit=cdn_redirect.cdn_file_hashes[0].limit limit=cdn_redirect.cdn_file_hashes[0].limit
)) ))
if isinstance(cdn_file, CdnFileReuploadNeeded): if isinstance(cdn_file, CdnFileReuploadNeeded):
# We need to use the original client here # We need to use the original client here
await client(ReuploadCdnFileRequest( client(ReuploadCdnFileRequest(
file_token=cdn_redirect.file_token, file_token=cdn_redirect.file_token,
request_token=cdn_file.request_token request_token=cdn_file.request_token
)) ))

View File

@ -13,8 +13,6 @@ class Factorization:
""" """
Factorizes the given large integer. 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. :param pq: the prime pair pq.
:return: a tuple containing the two factors p and q. :return: a tuple containing the two factors p and q.
""" """
@ -49,8 +47,7 @@ class Factorization:
if g > 1: if g > 1:
break break
p, q = g, pq // g return g, pq // g
return (p, q) if p < q else (q, p)
@staticmethod @staticmethod
def gcd(a, b): def gcd(a, b):

View File

@ -3,86 +3,15 @@ Helper module around the system's libssl library if available for IGE mode.
""" """
import ctypes import ctypes
import ctypes.util 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')
lib = ctypes.util.find_library('ssl') if not lib:
# 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 decrypt_ige = None
encrypt_ige = None encrypt_ige = None
else: else:
_libssl = ctypes.cdll.LoadLibrary(lib)
# https://github.com/openssl/openssl/blob/master/include/openssl/aes.h # https://github.com/openssl/openssl/blob/master/include/openssl/aes.h
AES_ENCRYPT = ctypes.c_int(1) AES_ENCRYPT = ctypes.c_int(1)
AES_DECRYPT = ctypes.c_int(0) AES_DECRYPT = ctypes.c_int(0)

View File

@ -14,7 +14,7 @@ except ImportError:
from ..tl import TLObject from ..tl import TLObject
# {fingerprint: (Crypto.PublicKey.RSA._RSAobj, old)} dictionary # {fingerprint: Crypto.PublicKey.RSA._RSAobj} dictionary
_server_keys = {} _server_keys = {}
@ -47,27 +47,26 @@ def _compute_fingerprint(key):
return struct.unpack('<q', sha1(n + e).digest()[-8:])[0] return struct.unpack('<q', sha1(n + e).digest()[-8:])[0]
def add_key(pub, *, old): def add_key(pub):
"""Adds a new public key to be used when encrypting new data is needed""" """Adds a new public key to be used when encrypting new data is needed"""
global _server_keys global _server_keys
key = rsa.PublicKey.load_pkcs1(pub) key = rsa.PublicKey.load_pkcs1(pub)
_server_keys[_compute_fingerprint(key)] = (key, old) _server_keys[_compute_fingerprint(key)] = key
def encrypt(fingerprint, data, *, use_old=False): def encrypt(fingerprint, data):
""" """
Encrypts the given data known the fingerprint to be used Encrypts the given data known the fingerprint to be used
in the way Telegram requires us to do so (sha1(data) + data + padding) in the way Telegram requires us to do so (sha1(data) + data + padding)
:param fingerprint: the fingerprint of the RSA key. :param fingerprint: the fingerprint of the RSA key.
:param data: the data to be encrypted. :param data: the data to be encrypted.
:param use_old: whether old keys should be used.
:return: :return:
the cipher text, or None if no key matching this fingerprint is found. the cipher text, or None if no key matching this fingerprint is found.
""" """
global _server_keys global _server_keys
key, old = _server_keys.get(fingerprint, [None, None]) key = _server_keys.get(fingerprint, None)
if (not key) or (old and not use_old): if not key:
return None return None
# len(sha1.digest) is always 20, so we're left with 255 - 20 - x padding # len(sha1.digest) is always 20, so we're left with 255 - 20 - x padding
@ -83,48 +82,6 @@ def encrypt(fingerprint, data, *, use_old=False):
# Add default keys # 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 ( for pub in (
'''-----BEGIN RSA PUBLIC KEY----- '''-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6 MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
@ -160,6 +117,6 @@ qAqBdmI1iBGdQv/OQCBcbXIWCGDY2AsiqLhlGQfPOI7/vvKc188rTriocgUtoTUc
/n/sIUzkgwTqRyvWYynWARWzQg0I9olLBBC2q5RQJJlnYXZwyTL3y9tdb7zOHkks /n/sIUzkgwTqRyvWYynWARWzQg0I9olLBBC2q5RQJJlnYXZwyTL3y9tdb7zOHkks
WV9IMQmZmyZh/N7sMbGWQpt4NMchGpPGeJ2e5gHBjDnlIf2p1yZOYeUYrdbwcS0t WV9IMQmZmyZh/N7sMbGWQpt4NMchGpPGeJ2e5gHBjDnlIf2p1yZOYeUYrdbwcS0t
UiggS4UeE8TzIuXFQxw7fzEIlmhIaq3FnwIDAQAB UiggS4UeE8TzIuXFQxw7fzEIlmhIaq3FnwIDAQAB
-----END RSA PUBLIC KEY-----''', -----END RSA PUBLIC KEY-----'''
): ):
add_key(pub, old=True) add_key(pub)

View File

@ -1 +0,0 @@
from .tl.custom import *

View File

@ -2,12 +2,13 @@
This module holds all the base and automatically generated errors that the This module holds all the base and automatically generated errors that the
Telegram API has. See telethon_generator/errors.json for more. Telegram API has. See telethon_generator/errors.json for more.
""" """
import urllib.request
import re import re
from threading import Thread
from .common import ( from .common import (
ReadCancelledError, TypeNotFoundError, InvalidChecksumError, ReadCancelledError, TypeNotFoundError, InvalidChecksumError,
InvalidBufferError, AuthKeyNotFound, SecurityError, CdnFileTamperedError, BrokenAuthKeyError, SecurityError, CdnFileTamperedError
AlreadyInConversationError, BadMessageError, MultiError
) )
# This imports the base errors too, as they're imported there # This imports the base errors too, as they're imported there
@ -15,32 +16,57 @@ from .rpcbaseerrors import *
from .rpcerrorlist import * from .rpcerrorlist import *
def rpc_message_to_error(rpc_error, request): def report_error(code, message, report_method):
"""
Reports an RPC error to pwrtelegram.
:param code: the integer code of the error (like 400).
:param message: the message representing the error.
:param report_method: the constructor ID of the function that caused it.
"""
try:
# Ensure it's signed
report_method = int.from_bytes(
report_method.to_bytes(4, 'big'), 'big', signed=True
)
url = urllib.request.urlopen(
'https://rpc.pwrtelegram.xyz?code={}&error={}&method={}'
.format(code, message, report_method),
timeout=5
)
url.read()
url.close()
except Exception as e:
"We really don't want to crash when just reporting an error"
def rpc_message_to_error(rpc_error, report_method=None):
""" """
Converts a Telegram's RPC Error to a Python error. Converts a Telegram's RPC Error to a Python error.
:param rpc_error: the RpcError instance. :param rpc_error: the RpcError instance.
:param request: the request that caused this error. :param report_method: if present, the ID of the method that caused it.
:return: the RPCError as a Python exception that represents this error. :return: the RPCError as a Python exception that represents this error.
""" """
if report_method is not None:
Thread(
target=report_error,
args=(rpc_error.error_code, rpc_error.error_message, report_method)
).start()
# Try to get the error by direct look-up, otherwise regex # 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, None)
cls = rpc_errors_dict.get(rpc_error.error_message.upper(), None)
if cls: if cls:
return cls(request=request) return cls()
for msg_regex, cls in rpc_errors_re: for msg_regex, cls in rpc_errors_re:
m = re.match(msg_regex, rpc_error.error_message) m = re.match(msg_regex, rpc_error.error_message)
if m: if m:
capture = int(m.group(1)) if m.groups() else None capture = int(m.group(1)) if m.groups() else None
return cls(request=request, capture=capture) return cls(capture=capture)
# Some errors are negative: cls = base_errors.get(rpc_error.error_code)
# * -500 for "No workers running", if cls:
# * -503 for "Timeout" return cls(rpc_error.error_message)
#
# We treat them as if they were positive, so -500 will be treated return RPCError(rpc_error.error_code, rpc_error.error_message)
# 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)

View File

@ -1,8 +1,4 @@
"""Errors not related to the Telegram API itself""" """Errors not related to the Telegram API itself"""
import struct
import textwrap
from ..tl import TLRequest
class ReadCancelledError(Exception): class ReadCancelledError(Exception):
@ -19,8 +15,8 @@ class TypeNotFoundError(Exception):
def __init__(self, invalid_constructor_id, remaining): def __init__(self, invalid_constructor_id, remaining):
super().__init__( super().__init__(
'Could not find a matching Constructor ID for the TLObject ' 'Could not find a matching Constructor ID for the TLObject '
'that was supposed to be read with ID {:08x}. See the FAQ ' 'that was supposed to be read with ID {:08x}. Most likely, '
'for more details. ' 'a TLObject was trying to be read when it should not be read. '
'Remaining bytes: {!r}'.format(invalid_constructor_id, remaining)) 'Remaining bytes: {!r}'.format(invalid_constructor_id, remaining))
self.invalid_constructor_id = invalid_constructor_id self.invalid_constructor_id = invalid_constructor_id
@ -42,37 +38,14 @@ class InvalidChecksumError(Exception):
self.valid_checksum = valid_checksum self.valid_checksum = valid_checksum
class InvalidBufferError(BufferError): class BrokenAuthKeyError(Exception):
""" """
Occurs when the buffer is invalid, and may contain an HTTP error code. Occurs when the authorization key for a data center is not valid.
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): def __init__(self):
super().__init__(textwrap.dedent(self.__class__.__doc__)) super().__init__(
'The authorization key is broken, and it must be reset.'
)
class SecurityError(Exception): class SecurityError(Exception):
@ -94,87 +67,3 @@ class CdnFileTamperedError(SecurityError):
super().__init__( super().__init__(
'The CDN file has been altered and its download cancelled.' '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

View File

@ -1,42 +1,15 @@
from ..tl import functions
_NESTS_QUERY = (
functions.InvokeAfterMsgRequest,
functions.InvokeAfterMsgsRequest,
functions.InitConnectionRequest,
functions.InvokeWithLayerRequest,
functions.InvokeWithoutUpdatesRequest,
functions.InvokeWithMessagesRangeRequest,
functions.InvokeWithTakeoutRequest,
)
class RPCError(Exception): class RPCError(Exception):
"""Base class for all Remote Procedure Call errors.""" """Base class for all Remote Procedure Call errors."""
code = None code = None
message = None message = None
def __init__(self, request, message, code=None): def __init__(self, code, message):
super().__init__('RPCError {}: {}{}'.format( super().__init__('RPCError {}: {}'.format(code, message))
code or self.code, message, self._fmt_request(request)))
self.request = request
self.code = code self.code = code
self.message = message 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): def __reduce__(self):
return type(self), (self.request, self.message, self.code) return type(self), (self.code, self.message)
class InvalidDCError(RPCError): class InvalidDCError(RPCError):
@ -74,6 +47,10 @@ class ForbiddenError(RPCError):
code = 403 code = 403
message = 'FORBIDDEN' message = 'FORBIDDEN'
def __init__(self, message):
super().__init__(message)
self.message = message
class NotFoundError(RPCError): class NotFoundError(RPCError):
""" """
@ -82,6 +59,10 @@ class NotFoundError(RPCError):
code = 404 code = 404
message = 'NOT_FOUND' message = 'NOT_FOUND'
def __init__(self, message):
super().__init__(message)
self.message = message
class AuthKeyError(RPCError): class AuthKeyError(RPCError):
""" """
@ -91,6 +72,10 @@ class AuthKeyError(RPCError):
code = 406 code = 406
message = 'AUTH_KEY' message = 'AUTH_KEY'
def __init__(self, message):
super().__init__(message)
self.message = message
class FloodError(RPCError): class FloodError(RPCError):
""" """
@ -109,23 +94,75 @@ class ServerError(RPCError):
for example, there was a disruption while accessing a database or file for example, there was a disruption while accessing a database or file
storage. storage.
""" """
code = 500 # Also witnessed as -500 code = 500
message = 'INTERNAL' message = 'INTERNAL'
def __init__(self, message):
super().__init__(message)
self.message = message
class TimedOutError(RPCError):
class BotTimeout(RPCError):
""" """
Clicking the inline buttons of bots that never (or take to long to) Clicking the inline buttons of bots that never (or take to long to)
call ``answerCallbackQuery`` will result in this "special" RPCError. call ``answerCallbackQuery`` will result in this "special" RPCError.
""" """
code = 503 # Only witnessed as -503 code = -503
message = 'Timeout' message = 'Timeout'
def __init__(self, message):
super().__init__(message)
self.message = message
BotTimeout = TimedOutError
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, code):
super().__init__(self.ErrorMessages.get(
code,
'Unknown error code (this should not happen): {}.'.format(code)))
self.code = code
base_errors = {x.code: x for x in ( base_errors = {x.code: x for x in (
InvalidDCError, BadRequestError, UnauthorizedError, ForbiddenError, InvalidDCError, BadRequestError, UnauthorizedError, ForbiddenError,
NotFoundError, AuthKeyError, FloodError, ServerError, TimedOutError NotFoundError, AuthKeyError, FloodError, ServerError, BotTimeout
)} )}

View File

@ -1,5 +1,4 @@
from .raw import Raw from .raw import Raw
from .album import Album
from .chataction import ChatAction from .chataction import ChatAction
from .messagedeleted import MessageDeleted from .messagedeleted import MessageDeleted
from .messageedited import MessageEdited from .messageedited import MessageEdited
@ -10,9 +9,6 @@ from .callbackquery import CallbackQuery
from .inlinequery import InlineQuery from .inlinequery import InlineQuery
_HANDLERS_ATTRIBUTE = '__tl.handlers'
class StopPropagation(Exception): class StopPropagation(Exception):
""" """
If this exception is raised in any of the handlers for a given event, If this exception is raised in any of the handlers for a given event,
@ -20,121 +16,20 @@ class StopPropagation(Exception):
It can be seen as the ``StopIteration`` in a for loop but for events. It can be seen as the ``StopIteration`` in a for loop but for events.
Example usage: Example usage:
>>> from telethon import TelegramClient, events >>> from telethon import TelegramClient, events
>>> client = TelegramClient(...) >>> client = TelegramClient(...)
>>> >>>
>>> @client.on(events.NewMessage) >>> @client.on(events.NewMessage)
... async def delete(event): ... def delete(event):
... await event.delete() ... event.delete()
... # No other event handler will have a chance to handle this event ... # No other event handler will have a chance to handle this event
... raise StopPropagation ... raise StopPropagation
... ...
>>> @client.on(events.NewMessage) >>> @client.on(events.NewMessage)
... async def _(event): ... def _(event):
... # Will never be reached, because it is the second handler ... # Will never be reached, because it is the second handler
... pass ... pass
""" """
# For some reason Sphinx wants the silly >>> or # For some reason Sphinx wants the silly >>> or
# it will show warnings and look bad when generated. # it will show warnings and look bad when generated.
pass 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)

View File

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

View File

@ -1,5 +1,4 @@
import re import re
import struct
from .common import EventBuilder, EventCommon, name_inner_event from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils from .. import utils
@ -10,8 +9,7 @@ from ..tl.custom.sendergetter import SenderGetter
@name_inner_event @name_inner_event
class CallbackQuery(EventBuilder): class CallbackQuery(EventBuilder):
""" """
Occurs whenever you sign in as a bot and a user Represents a callback query event (when an inline button is clicked).
clicks one of the inline buttons on your messages.
Note that the `chats` parameter will **not** work with normal Note that the `chats` parameter will **not** work with normal
IDs or peers if the clicked inline button comes from a "via bot" IDs or peers if the clicked inline button comes from a "via bot"
@ -19,109 +17,58 @@ class CallbackQuery(EventBuilder):
`chat_instance` which should be used for inline callbacks. `chat_instance` which should be used for inline callbacks.
Args: Args:
data (`bytes`, `str`, `callable`, optional): data (`bytes` | `str` | `callable`, optional):
If set, the inline button payload data must match this data. 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 A UTF-8 string can also be given, a regex or a callable. For
instance, to check against ``'data_1'`` and ``'data_2'`` you instance, to check against ``'data_1'`` and ``'data_2'`` you
can use ``re.compile(b'data_')``. 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__( def __init__(self, chats=None, *, blacklist_chats=False, data=None):
self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None): super().__init__(chats=chats, blacklist_chats=blacklist_chats)
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
if data and pattern: if isinstance(data, bytes):
raise ValueError("Only pass either data or pattern not both.") self.data = data
elif isinstance(data, str):
self.data = data.encode('utf-8')
elif not data or callable(data):
self.data = data
elif hasattr(data, 'match') and callable(data.match):
if not isinstance(getattr(data, 'pattern', b''), bytes):
data = re.compile(data.pattern.encode('utf-8'), data.flags)
if isinstance(data, str): self.data = data.match
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: else:
raise TypeError('Invalid data or pattern type given') raise TypeError('Invalid data type given')
self._no_check = all(x is None for x in (
self.chats, self.func, self.match,
))
@classmethod @classmethod
def build(cls, update, others=None, self_id=None): def build(cls, update):
if isinstance(update, types.UpdateBotCallbackQuery): if isinstance(update, (types.UpdateBotCallbackQuery,
return cls.Event(update, update.peer, update.msg_id) types.UpdateInlineBotCallbackQuery)):
elif isinstance(update, types.UpdateInlineBotCallbackQuery): event = cls.Event(update)
# See https://github.com/LonamiWebs/Telethon/pull/1005 else:
# The long message ID is actually just msg_id + peer_id return
mid, pid = struct.unpack('<ii', struct.pack('<q', update.msg_id.id))
peer = types.PeerChannel(-pid) if pid < 0 else types.PeerUser(pid) event._entities = update._entities
return cls.Event(update, peer, mid) return event
def filter(self, event): 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: if self.chats is not None:
inside = event.query.chat_instance in self.chats inside = event.query.chat_instance in self.chats
if event.chat_id: if event.chat_id:
inside |= event.chat_id in self.chats inside |= event.chat_id in self.chats
if inside == self.blacklist_chats: if inside == self.blacklist_chats:
return return None
if self.match: if self.data:
if callable(self.match): if callable(self.data):
event.data_match = event.pattern_match = self.match(event.query.data) event.data_match = self.data(event.query.data)
if not event.data_match: if not event.data_match:
return return None
elif event.query.data != self.match: elif event.query.data != self.data:
return return None
if self.func: return event
# Return the result of func directly as it may need to be awaited
return self.func(event)
return True
class Event(EventCommon, SenderGetter): class Event(EventCommon, SenderGetter):
""" """
@ -135,24 +82,18 @@ class CallbackQuery(EventBuilder):
The object returned by the ``data=`` parameter The object returned by the ``data=`` parameter
when creating the event builder, if any. Similar when creating the event builder, if any. Similar
to ``pattern_match`` for the new message event. to ``pattern_match`` for the new message event.
pattern_match (`obj`, optional):
Alias for ``data_match``.
""" """
def __init__(self, query, peer, msg_id): def __init__(self, query):
super().__init__(peer, msg_id=msg_id) super().__init__(chat_peer=getattr(query, 'peer', None),
SenderGetter.__init__(self, query.user_id) msg_id=query.msg_id)
self.query = query self.query = query
self.data_match = None self.data_match = None
self.pattern_match = None self._sender_id = query.user_id
self._input_sender = None
self._sender = None
self._message = None self._message = None
self._answered = False 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 @property
def id(self): def id(self):
""" """
@ -166,7 +107,7 @@ class CallbackQuery(EventBuilder):
""" """
Returns the message ID to which the clicked inline button belongs. Returns the message ID to which the clicked inline button belongs.
""" """
return self._message_id return self.query.msg_id
@property @property
def data(self): def data(self):
@ -183,7 +124,7 @@ class CallbackQuery(EventBuilder):
""" """
return self.query.chat_instance return self.query.chat_instance
async def get_message(self): def get_message(self):
""" """
Returns the message to which the clicked inline button belongs. Returns the message to which the clicked inline button belongs.
""" """
@ -191,15 +132,15 @@ class CallbackQuery(EventBuilder):
return self._message return self._message
try: try:
chat = await self.get_input_chat() if self.is_channel else None chat = self.get_input_chat() if self.is_channel else None
self._message = await self._client.get_messages( self._message = self._client.get_messages(
chat, ids=self._message_id) chat, ids=self.query.msg_id)
except ValueError: except ValueError:
return return
return self._message return self._message
async def _refetch_sender(self): def _refetch_sender(self):
self._sender = self._entities.get(self.sender_id) self._sender = self._entities.get(self.sender_id)
if not self._sender: if not self._sender:
return return
@ -208,15 +149,16 @@ class CallbackQuery(EventBuilder):
if not getattr(self._input_sender, 'access_hash', True): if not getattr(self._input_sender, 'access_hash', True):
# getattr with True to handle the InputPeerSelf() case # getattr with True to handle the InputPeerSelf() case
try: try:
self._input_sender = self._client._mb_entity_cache.get( self._input_sender = self._client.session.get_input_entity(
utils.resolve_id(self._sender_id)[0])._as_input_peer() self._sender_id
except AttributeError: )
m = await self.get_message() except ValueError:
m = self.get_message()
if m: if m:
self._sender = m._sender self._sender = m._sender
self._input_sender = m._input_sender self._input_sender = m._input_sender
async def answer( def answer(
self, message=None, cache_time=0, *, url=None, alert=False): self, message=None, cache_time=0, *, url=None, alert=False):
""" """
Answers the callback query (and stops the loading circle). Answers the callback query (and stops the loading circle).
@ -236,13 +178,13 @@ class CallbackQuery(EventBuilder):
alert (`bool`, optional): alert (`bool`, optional):
Whether an alert (a pop-up dialog) should be used Whether an alert (a pop-up dialog) should be used
instead of showing a toast. Defaults to `False`. instead of showing a toast. Defaults to ``False``.
""" """
if self._answered: if self._answered:
return return
self._answered = True self._answered = True
return await self._client( return self._client(
functions.messages.SetBotCallbackAnswerRequest( functions.messages.SetBotCallbackAnswerRequest(
query_id=self.query.query_id, query_id=self.query.query_id,
cache_time=cache_time, cache_time=cache_time,
@ -252,95 +194,61 @@ class CallbackQuery(EventBuilder):
) )
) )
@property def respond(self, *args, **kwargs):
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 Responds to the message (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with `telethon.telegram_client.TelegramClient.send_message` with
``entity`` already set. ``entity`` already set.
This method also creates a task to `answer` the callback. 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()) self._client.loop.create_task(self.answer())
return await self._client.send_message( return self._client.send_message(
await self.get_input_chat(), *args, **kwargs) self.get_input_chat(), *args, **kwargs)
async def reply(self, *args, **kwargs): def reply(self, *args, **kwargs):
""" """
Replies to the message (as a reply). Shorthand for Replies to the message (as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with `telethon.telegram_client.TelegramClient.send_message` with
both ``entity`` and ``reply_to`` already set. both ``entity`` and ``reply_to`` already set.
This method also creates a task to `answer` the callback. 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()) self._client.loop.create_task(self.answer())
kwargs['reply_to'] = self.query.msg_id kwargs['reply_to'] = self.query.msg_id
return await self._client.send_message( return self._client.send_message(
await self.get_input_chat(), *args, **kwargs) self.get_input_chat(), *args, **kwargs)
async def edit(self, *args, **kwargs): def edit(self, *args, **kwargs):
""" """
Edits the message. Shorthand for Edits the message iff it's outgoing. Shorthand for
`telethon.client.messages.MessageMethods.edit_message` with `telethon.telegram_client.TelegramClient.edit_message` with
the ``entity`` set to the correct :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64`. both ``entity`` and ``message`` already set.
Returns `True` if the edit was successful. Returns the edited :tl:`Message`.
This method also creates a task to `answer` the callback. 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()) self._client.loop.create_task(self.answer())
if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): return self._client.edit_message(
return await self._client.edit_message( self.get_input_chat(), self.query.msg_id,
self.query.msg_id, *args, **kwargs *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): def delete(self, *args, **kwargs):
""" """
Deletes the message. Shorthand for Deletes the message. Shorthand for
`telethon.client.messages.MessageMethods.delete_messages` with `telethon.telegram_client.TelegramClient.delete_messages` with
``entity`` and ``message_ids`` already set. ``entity`` and ``message_ids`` already set.
If you need to delete more than one message at once, don't use If you need to delete more than one message at once, don't use
this `delete` method. Use a this `delete` method. Use a
`telethon.client.telegramclient.TelegramClient` instance directly. `telethon.telegram_client.TelegramClient` instance directly.
This method also creates a task to `answer` the callback. 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()) self._client.loop.create_task(self.answer())
if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): return self._client.delete_messages(
raise TypeError('Inline messages cannot be deleted as there is no API request available to do so') self.get_input_chat(), [self.query.msg_id],
return await self._client.delete_messages(
await self.get_input_chat(), [self.query.msg_id],
*args, **kwargs *args, **kwargs
) )

View File

@ -1,64 +1,31 @@
from .common import EventBuilder, EventCommon, name_inner_event from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils from .. import utils
from ..tl import types from ..tl import types, functions, custom
@name_inner_event @name_inner_event
class ChatAction(EventBuilder): class ChatAction(EventBuilder):
""" """
Occurs on certain chat actions: Represents an action in a chat (such as user joined, left, or new pin).
* 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 @classmethod
def build(cls, update, others=None, self_id=None): def build(cls, update):
# Rely on specific pin updates for unpins, but otherwise ignore them if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0:
# for new pins (we'd rather handle the new service message with pin, # Telegram does not always send
# so that we can act on that message'). # UpdateChannelPinnedMessage for new pins
if isinstance(update, types.UpdatePinnedChannelMessages) and not update.pinned: # but always for unpin, with update.id = 0
return cls.Event(types.PeerChannel(update.channel_id), event = cls.Event(types.PeerChannel(update.channel_id),
pin_ids=update.messages, unpin=True)
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): elif isinstance(update, types.UpdateChatParticipantAdd):
return cls.Event(types.PeerChat(update.chat_id), event = cls.Event(types.PeerChat(update.chat_id),
added_by=update.inviter_id or True, added_by=update.inviter_id or True,
users=update.user_id) users=update.user_id)
elif isinstance(update, types.UpdateChatParticipantDelete): elif isinstance(update, types.UpdateChatParticipantDelete):
return cls.Event(types.PeerChat(update.chat_id), event = cls.Event(types.PeerChat(update.chat_id),
kicked_by=True, kicked_by=True,
users=update.user_id) 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, ( elif (isinstance(update, (
types.UpdateNewMessage, types.UpdateNewChannelMessage)) types.UpdateNewMessage, types.UpdateNewChannelMessage))
@ -66,117 +33,105 @@ class ChatAction(EventBuilder):
msg = update.message msg = update.message
action = update.message.action action = update.message.action
if isinstance(action, types.MessageActionChatJoinedByLink): if isinstance(action, types.MessageActionChatJoinedByLink):
return cls.Event(msg, event = cls.Event(msg,
added_by=True, added_by=True,
users=msg.from_id) users=msg.from_id)
elif isinstance(action, types.MessageActionChatAddUser): elif isinstance(action, types.MessageActionChatAddUser):
# If a user adds itself, it means they joined via the public chat username # If an user adds itself, it means they joined
added_by = ([msg.sender_id] == action.users) or msg.from_id added_by = ([msg.from_id] == action.users) or msg.from_id
return cls.Event(msg, event = cls.Event(msg,
added_by=added_by, added_by=added_by,
users=action.users) users=action.users)
elif isinstance(action, types.MessageActionChatDeleteUser): elif isinstance(action, types.MessageActionChatDeleteUser):
return cls.Event(msg, event = cls.Event(msg,
kicked_by=utils.get_peer_id(msg.from_id) if msg.from_id else True, kicked_by=msg.from_id or True,
users=action.user_id) users=action.user_id)
elif isinstance(action, types.MessageActionChatCreate): elif isinstance(action, types.MessageActionChatCreate):
return cls.Event(msg, event = cls.Event(msg,
users=action.users, users=action.users,
created=True, created=True,
new_title=action.title) new_title=action.title)
elif isinstance(action, types.MessageActionChannelCreate): elif isinstance(action, types.MessageActionChannelCreate):
return cls.Event(msg, event = cls.Event(msg,
created=True, created=True,
users=msg.from_id, users=msg.from_id,
new_title=action.title) new_title=action.title)
elif isinstance(action, types.MessageActionChatEditTitle): elif isinstance(action, types.MessageActionChatEditTitle):
return cls.Event(msg, event = cls.Event(msg,
users=msg.from_id, users=msg.from_id,
new_title=action.title) new_title=action.title)
elif isinstance(action, types.MessageActionChatEditPhoto): elif isinstance(action, types.MessageActionChatEditPhoto):
return cls.Event(msg, event = cls.Event(msg,
users=msg.from_id, users=msg.from_id,
new_photo=action.photo) new_photo=action.photo)
elif isinstance(action, types.MessageActionChatDeletePhoto): elif isinstance(action, types.MessageActionChatDeletePhoto):
return cls.Event(msg, event = cls.Event(msg,
users=msg.from_id, users=msg.from_id,
new_photo=True) new_photo=True)
elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to: elif isinstance(action, types.MessageActionPinMessage):
return cls.Event(msg, # Telegram always sends this service message for new pins
pin_ids=[msg.reply_to_msg_id]) event = cls.Event(msg,
elif isinstance(action, types.MessageActionGameScore): users=msg.from_id,
return cls.Event(msg, new_pin=msg.reply_to_msg_id)
new_score=action.score) else:
return
else:
return
elif isinstance(update, types.UpdateChannelParticipant) \ event._entities = update._entities
and bool(update.new_participant) != bool(update.prev_participant): return event
# 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): class Event(EventCommon):
""" """
Represents the event of a new chat action. Represents the event of a new chat action.
Members: Members:
action_message (`MessageAction <https://tl.telethon.dev/types/message_action.html>`_): action_message (`MessageAction <https://lonamiwebs.github.io/Telethon/types/message_action.html>`_):
The message invoked by this Chat Action. The message invoked by this Chat Action.
new_pin (`bool`): new_pin (`bool`):
`True` if there is a new pin. ``True`` if there is a new pin.
new_photo (`bool`): new_photo (`bool`):
`True` if there's a new chat photo (or it was removed). ``True`` if there's a new chat photo (or it was removed).
photo (:tl:`Photo`, optional): photo (:tl:`Photo`, optional):
The new photo (or `None` if it was removed). The new photo (or ``None`` if it was removed).
user_added (`bool`): user_added (`bool`):
`True` if the user was added by some other. ``True`` if the user was added by some other.
user_joined (`bool`): user_joined (`bool`):
`True` if the user joined on their own. ``True`` if the user joined on their own.
user_left (`bool`): user_left (`bool`):
`True` if the user left on their own. ``True`` if the user left on their own.
user_kicked (`bool`): user_kicked (`bool`):
`True` if the user was kicked by some other. ``True`` if the user was kicked by some other.
created (`bool`, optional): created (`bool`, optional):
`True` if this chat was just created. ``True`` if this chat was just created.
new_title (`str`, optional): new_title (`str`, optional):
The new title string for the chat, if applicable. The new title string for the chat, if applicable.
new_score (`str`, optional):
The new score string for the game, if applicable.
unpin (`bool`): unpin (`bool`):
`True` if the existing pin gets unpinned. ``True`` if the existing pin gets unpinned.
""" """
def __init__(self, where, new_pin=None, new_photo=None,
def __init__(self, where, new_photo=None,
added_by=None, kicked_by=None, created=None, added_by=None, kicked_by=None, created=None,
users=None, new_title=None, pin_ids=None, pin=None, new_score=None): users=None, new_title=None, unpin=None):
if isinstance(where, types.MessageService): if isinstance(where, types.MessageService):
self.action_message = where self.action_message = where
where = where.peer_id where = where.to_id
else: else:
self.action_message = None self.action_message = None
# TODO needs some testing (can there be more than one id, and do they follow pin order?) super().__init__(chat_peer=where, msg_id=new_pin)
# 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.new_pin = isinstance(new_pin, int)
self._pin_ids = pin_ids self._pinned_message = new_pin
self._pinned_messages = None
self.new_photo = new_photo is not None self.new_photo = new_photo is not None
self.photo = \ self.photo = \
@ -184,8 +139,8 @@ class ChatAction(EventBuilder):
self._added_by = None self._added_by = None
self._kicked_by = None self._kicked_by = None
self.user_added = self.user_joined = self.user_left = \ self.user_added, self.user_joined, self.user_left,\
self.user_kicked = self.unpin = False self.user_kicked, self.unpin = (False, False, False, False, False)
if added_by is True: if added_by is True:
self.user_joined = True self.user_joined = True
@ -193,64 +148,54 @@ class ChatAction(EventBuilder):
self.user_added = True self.user_added = True
self._added_by = added_by self._added_by = added_by
# If `from_id` was not present (it's `True`) or the affected if kicked_by is True:
# 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 self.user_left = True
elif kicked_by: elif kicked_by:
self.user_kicked = True self.user_kicked = True
self._kicked_by = kicked_by self._kicked_by = kicked_by
self.created = bool(created) self.created = bool(created)
self._user_peers = users if isinstance(users, list) else [users]
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._users = None
self._input_users = None self._input_users = None
self.new_title = new_title self.new_title = new_title
self.new_score = new_score self.unpin = unpin
self.unpin = not pin
def _set_client(self, client): def _set_client(self, client):
super()._set_client(client) super()._set_client(client)
if self.action_message: if self.action_message:
self.action_message._finish_init(client, self._entities, None) self.action_message._finish_init(client, self._entities, None)
async def respond(self, *args, **kwargs): def respond(self, *args, **kwargs):
""" """
Responds to the chat action message (not as a reply). Shorthand for Responds to the chat action message (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with `telethon.telegram_client.TelegramClient.send_message` with
``entity`` already set. ``entity`` already set.
""" """
return await self._client.send_message( return self._client.send_message(
await self.get_input_chat(), *args, **kwargs) self.get_input_chat(), *args, **kwargs)
async def reply(self, *args, **kwargs): def reply(self, *args, **kwargs):
""" """
Replies to the chat action message (as a reply). Shorthand for Replies to the chat action message (as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with `telethon.telegram_client.TelegramClient.send_message` with
both ``entity`` and ``reply_to`` already set. both ``entity`` and ``reply_to`` already set.
Has the same effect as `respond` if there is no message. Has the same effect as `respond` if there is no message.
""" """
if not self.action_message: if not self.action_message:
return await self.respond(*args, **kwargs) return self.respond(*args, **kwargs)
kwargs['reply_to'] = self.action_message.id kwargs['reply_to'] = self.action_message.id
return await self._client.send_message( return self._client.send_message(
await self.get_input_chat(), *args, **kwargs) self.get_input_chat(), *args, **kwargs)
async def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
""" """
Deletes the chat action message. You're responsible for checking Deletes the chat action message. You're responsible for checking
whether you have the permission to do so, or to except the error whether you have the permission to do so, or to except the error
otherwise. Shorthand for otherwise. Shorthand for
`telethon.client.messages.MessageMethods.delete_messages` with `telethon.telegram_client.TelegramClient.delete_messages` with
``entity`` and ``message_ids`` already set. ``entity`` and ``message_ids`` already set.
Does nothing if no message action triggered this event. Does nothing if no message action triggered this event.
@ -258,41 +203,40 @@ class ChatAction(EventBuilder):
if not self.action_message: if not self.action_message:
return return
return await self._client.delete_messages( return self._client.delete_messages(
await self.get_input_chat(), [self.action_message], self.get_input_chat(), [self.action_message],
*args, **kwargs *args, **kwargs
) )
async def get_pinned_message(self): def get_pinned_message(self):
""" """
If ``new_pin`` is `True`, this returns the `Message If ``new_pin`` is ``True``, this returns the
<telethon.tl.custom.message.Message>` object that was pinned. `telethon.tl.custom.message.Message` object that was pinned.
""" """
if self._pinned_messages is None: if self._pinned_message == 0:
await self.get_pinned_messages() return None
if self._pinned_messages: if isinstance(self._pinned_message, int)\
return self._pinned_messages[0] and self.get_input_chat():
r = self._client(functions.channels.GetMessagesRequest(
self._input_chat, [self._pinned_message]
))
try:
self._pinned_message = next(
x for x in r.messages
if isinstance(x, types.Message)
and x.id == self._pinned_message
)
except StopIteration:
pass
async def get_pinned_messages(self): if isinstance(self._pinned_message, types.Message):
""" return self._pinned_message
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 @property
def added_by(self): def added_by(self):
""" """
The user who added ``users``, if applicable (`None` otherwise). The user who added ``users``, if applicable (``None`` otherwise).
""" """
if self._added_by and not isinstance(self._added_by, types.User): if self._added_by and not isinstance(self._added_by, types.User):
aby = self._entities.get(utils.get_peer_id(self._added_by)) aby = self._entities.get(utils.get_peer_id(self._added_by))
@ -301,19 +245,19 @@ class ChatAction(EventBuilder):
return self._added_by return self._added_by
async def get_added_by(self): def get_added_by(self):
""" """
Returns `added_by` but will make an API call if necessary. Returns `added_by` but will make an API call if necessary.
""" """
if not self.added_by and self._added_by: if not self.added_by and self._added_by:
self._added_by = await self._client.get_entity(self._added_by) self._added_by = self._client.get_entity(self._added_by)
return self._added_by return self._added_by
@property @property
def kicked_by(self): def kicked_by(self):
""" """
The user who kicked ``users``, if applicable (`None` otherwise). The user who kicked ``users``, if applicable (``None`` otherwise).
""" """
if self._kicked_by and not isinstance(self._kicked_by, types.User): if self._kicked_by and not isinstance(self._kicked_by, types.User):
kby = self._entities.get(utils.get_peer_id(self._kicked_by)) kby = self._entities.get(utils.get_peer_id(self._kicked_by))
@ -322,34 +266,33 @@ class ChatAction(EventBuilder):
return self._kicked_by return self._kicked_by
async def get_kicked_by(self): def get_kicked_by(self):
""" """
Returns `kicked_by` but will make an API call if necessary. Returns `kicked_by` but will make an API call if necessary.
""" """
if not self.kicked_by and self._kicked_by: if not self.kicked_by and self._kicked_by:
self._kicked_by = await self._client.get_entity(self._kicked_by) self._kicked_by = self._client.get_entity(self._kicked_by)
return self._kicked_by return self._kicked_by
@property @property
def user(self): def user(self):
""" """
The first user that takes part in this action. For example, who joined. The first user that takes part in this action (e.g. joined).
Might be `None` if the information can't be retrieved or Might be ``None`` if the information can't be retrieved or
there is no user taking part. there is no user taking part.
""" """
if self.users: if self.users:
return self._users[0] return self._users[0]
async def get_user(self): def get_user(self):
""" """
Returns `user` but will make an API call if necessary. Returns `user` but will make an API call if necessary.
""" """
if self.users or await self.get_users(): if self.users or self.get_users():
return self._users[0] return self._users[0]
@property
def input_user(self): def input_user(self):
""" """
Input version of the ``self.user`` property. Input version of the ``self.user`` property.
@ -357,11 +300,11 @@ class ChatAction(EventBuilder):
if self.input_users: if self.input_users:
return self._input_users[0] return self._input_users[0]
async def get_input_user(self): def get_input_user(self):
""" """
Returns `input_user` but will make an API call if necessary. Returns `input_user` but will make an API call if necessary.
""" """
if self.input_users or await self.get_input_users(): if self.input_users or self.get_input_users():
return self._input_users[0] return self._input_users[0]
@property @property
@ -369,42 +312,51 @@ class ChatAction(EventBuilder):
""" """
Returns the marked signed ID of the first user, if any. Returns the marked signed ID of the first user, if any.
""" """
if self._user_ids: if self._user_peers:
return self._user_ids[0] return utils.get_peer_id(self._user_peers[0])
@property @property
def users(self): def users(self):
""" """
A list of users that take part in this action. For example, who joined. A list of users that take part in this action (e.g. joined).
Might be empty if the information can't be retrieved or there Might be empty if the information can't be retrieved or there
are no users taking part. are no users taking part.
""" """
if not self._user_ids: if not self._user_peers:
return [] return []
if self._users is None: if self._users is None:
self._users = [ self._users = [
self._entities[user_id] self._entities[utils.get_peer_id(peer)]
for user_id in self._user_ids for peer in self._user_peers
if user_id in self._entities if utils.get_peer_id(peer) in self._entities
] ]
return self._users return self._users
async def get_users(self): def get_users(self):
""" """
Returns `users` but will make an API call if necessary. Returns `users` but will make an API call if necessary.
""" """
if not self._user_ids: if not self._user_peers:
return [] 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_peers):
if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message: have, missing = [], []
await self.action_message._reload_message() for peer in self._user_peers:
self._users = [ user = self._entities.get(utils.get_peer_id(peer))
u for u in self.action_message.action_entities if user:
if isinstance(u, (types.User, types.UserEmpty))] have.append(user)
else:
missing.append(peer)
try:
missing = self._client.get_entity(missing)
except (TypeError, ValueError):
missing = []
self._users = have + missing
return self._users return self._users
@ -413,46 +365,28 @@ class ChatAction(EventBuilder):
""" """
Input version of the ``self.users`` property. Input version of the ``self.users`` property.
""" """
if self._input_users is None and self._user_ids: if self._input_users is None and self._user_peers:
self._input_users = [] self._input_users = []
for user_id in self._user_ids: for peer in self._user_peers:
# First try to get it from our entities
try: try:
self._input_users.append(utils.get_input_peer(self._entities[user_id])) self._input_users.append(
continue self._client.session.get_input_entity(peer)
except (KeyError, TypeError): )
except ValueError:
pass 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 [] return self._input_users or []
async def get_input_users(self): def get_input_users(self):
""" """
Returns `input_users` but will make an API call if necessary. Returns `input_users` but will make an API call if necessary.
""" """
if not self._user_ids: # TODO Maybe we could re-fetch the message
return [] return self.input_users
# 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 @property
def user_ids(self): def user_ids(self):
""" """
Returns the marked signed ID of the users, if any. Returns the marked signed ID of the users, if any.
""" """
if self._user_ids: if self._user_peers:
return self._user_ids[:] return [utils.get_peer_id(u) for u in self._user_peers]

View File

@ -1,5 +1,4 @@
import abc import abc
import asyncio
import warnings import warnings
from .. import utils from .. import utils
@ -7,7 +6,7 @@ from ..tl import TLObject, types
from ..tl.custom.chatgetter import ChatGetter from ..tl.custom.chatgetter import ChatGetter
async def _into_id_set(client, chats): def _into_id_set(client, chats):
"""Helper util to turn the input chat or chats into a set of IDs.""" """Helper util to turn the input chat or chats into a set of IDs."""
if chats is None: if chats is None:
return None return None
@ -30,9 +29,9 @@ async def _into_id_set(client, chats):
# 0x2d45687 == crc32(b'Peer') # 0x2d45687 == crc32(b'Peer')
result.add(utils.get_peer_id(chat)) result.add(utils.get_peer_id(chat))
else: else:
chat = await client.get_input_entity(chat) chat = client.get_input_entity(chat)
if isinstance(chat, types.InputPeerSelf): if isinstance(chat, types.InputPeerSelf):
chat = await client.get_me(input_peer=True) chat = client.get_me(input_peer=True)
result.add(utils.get_peer_id(chat)) result.add(utils.get_peer_id(chat))
return result return result
@ -52,79 +51,37 @@ class EventBuilder(abc.ABC):
as a whitelist (default). This means that every chat as a whitelist (default). This means that every chat
will be handled *except* those specified in ``chats`` will be handled *except* those specified in ``chats``
which will be ignored if ``blacklist_chats=True``. 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_id = None
def __init__(self, chats=None, blacklist_chats=False):
self.chats = chats self.chats = chats
self.blacklist_chats = bool(blacklist_chats) self.blacklist_chats = blacklist_chats
self.resolved = False self._self_id = None
self.func = func
self._resolve_lock = None
@classmethod @classmethod
@abc.abstractmethod @abc.abstractmethod
def build(cls, update, others=None, self_id=None): def build(cls, update):
""" """Builds an event for the given update if possible, or returns None"""
Builds an event for the given update if possible, or returns None.
`others` are the rest of updates that came in the same container def resolve(self, client):
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""" """Helper method to allow event builders to be resolved before usage"""
if self.resolved: self.chats = _into_id_set(client, self.chats)
return if not EventBuilder.self_id:
EventBuilder.self_id = client.get_peer_id('me')
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): def filter(self, event):
""" """
Returns a truthy value if the event passed the filter and should be If the ID of ``event._chat_peer`` isn't in the chats set (or it is
used, or falsy otherwise. The return value may need to be awaited. but the set is a blacklist) returns ``None``, otherwise the event.
The events must have been resolved before this can be called.
""" """
if not self.resolved:
return
if self.chats is not None: if self.chats is not None:
# Note: the `event.chat_id` property checks if it's `None` for us inside = utils.get_peer_id(event._chat_peer) in self.chats
inside = event.chat_id in self.chats
if inside == self.blacklist_chats: if inside == self.blacklist_chats:
# If this chat matches but it's a blacklist ignore. # If this chat matches but it's a blacklist ignore.
# If it doesn't match but it's a whitelist ignore. # If it doesn't match but it's a whitelist ignore.
return return None
return event
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): class EventCommon(ChatGetter, abc.ABC):
@ -140,11 +97,14 @@ class EventCommon(ChatGetter, abc.ABC):
""" """
_event_name = 'Event' _event_name = 'Event'
def __init__(self, chat_peer=None, msg_id=None, broadcast=None): def __init__(self, chat_peer=None, msg_id=None, broadcast=False):
super().__init__(chat_peer, broadcast=broadcast)
self._entities = {} self._entities = {}
self._client = None self._client = None
self._chat_peer = chat_peer
self._message_id = msg_id self._message_id = msg_id
self._input_chat = None
self._chat = None
self._broadcast = broadcast
self.original_update = None self.original_update = None
def _set_client(self, client): def _set_client(self, client):
@ -152,11 +112,19 @@ class EventCommon(ChatGetter, abc.ABC):
Setter so subclasses can act accordingly when the client is set. Setter so subclasses can act accordingly when the client is set.
""" """
self._client = client self._client = client
if self._chat_peer: self._chat = self._entities.get(self.chat_id)
self._chat, self._input_chat = utils._get_entity_pair( if not self._chat:
self.chat_id, self._entities, client._mb_entity_cache) return
else:
self._chat = self._input_chat = None self._input_chat = utils.get_input_peer(self._chat)
if not getattr(self._input_chat, 'access_hash', True):
# getattr with True to handle the InputPeerSelf() case
try:
self._input_chat = self._client.session.get_input_entity(
self._chat_peer
)
except ValueError:
self._input_chat = None
@property @property
def client(self): def client(self):

Some files were not shown because too many files have changed in this diff Show More