Clean up code

This commit is contained in:
udf 2019-06-27 16:00:19 +02:00
parent 2a7d4317bd
commit c1c985e106
No known key found for this signature in database
GPG Key ID: E40724BAD73AF77B
188 changed files with 0 additions and 35735 deletions

View File

@ -1,27 +0,0 @@
---
name: Bug Report
about: Create a report about a bug inside the library or issues with the documentation
title: ''
labels: ''
assignees: ''
---
**Checklist**
* [ ] The error is in the library's code, and not in my own.
* [ ] I have searched for this issue before posting it and there isn't a duplicate.
* [ ] I ran `pip install -U https://github.com/LonamiWebs/Telethon/archive/master.zip` and triggered the bug in the latest version.
**Code that causes the issue**
```python
from telethon.sync import TelegramClient
...
```
**Traceback**
```
Traceback (most recent call last):
File "code.py", line 1, in <code>
```

View File

@ -1,10 +0,0 @@
---
name: Feature Request
about: Suggest ideas, changes or other enhancements for the library
title: ''
labels: enhancement
assignees: ''
---
Please describe your idea. Would you like another friendly method? Renaming them to something more appropriated? Changing the way something works?

View File

@ -1,20 +0,0 @@
---
name: Question about Usage
about: QUESTIONS DON'T BELONG HERE. Ask in StackOverflow or in @TelethonUpdates
title: ''
labels: RTFM
assignees: ''
---
QUESTIONS ARE NEITHER BUGS NOR ENHANCEMENTS AND DON'T BELONG HERE.
If you DO have a question, ask in:
* https://stackoverflow.com or
* https://t.me/TelethonUpdates (@TelethonUpdates channel in Telegram)
If you do post a question, it will be labelled "RTFM" and closed as soon as possible without any answer.
If you DON'T have a question, use the right template for bugs/issues with the library or to propose an improvement/enhancement to either the code or documentation.
We are not being harsh. Only clear. The issues section is not for questions, and people keep asking things over and over, which is a waste of everyone's time.

108
.gitignore vendored
View File

@ -1,108 +0,0 @@
# Docs
_build/
docs/
# Generated code
telethon/tl/functions/
telethon/tl/types/
telethon/tl/patched/
telethon/tl/alltlobjects.py
telethon/errors/rpcerrorlist.py
# User session
*.session
usermedia/
# Quick tests should live in this file
example.py
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.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,22 +0,0 @@
- repo: git://github.com/pre-commit/pre-commit-hooks
sha: 7539d8bd1a00a3c1bfd34cdb606d3a6372e83469
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: double-quote-string-fixer
- id: end-of-file-fixer
- id: name-tests-test
- id: trailing-whitespace
- repo: git://github.com/pre-commit/mirrors-yapf
sha: v0.11.1
hooks:
- id: yapf
- repo: git://github.com/FalconSocial/pre-commit-python-sorter
sha: 1.0.4
hooks:
- id: python-import-sorter
args:
- --silent-overwrite

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2016-2019 LonamiWebs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

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

View File

@ -1,83 +0,0 @@
Telethon
========
.. epigraph::
⭐️ Thanks **everyone** who has starred the project, it means a lot!
|logo| **Telethon** is an asyncio_ **Python 3**
MTProto_ library to interact with Telegram_'s API
as a user or through a bot account (bot API alternative).
.. important::
If you have code using Telethon before its 1.0 version, you must
read `Compatibility and Convenience`_ to learn how to migrate.
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/latest/misc/compatibility-and-convenience.html
.. _Read The Docs: https://docs.telethon.dev
.. |logo| image:: logo.svg
:width: 24pt
:height: 24pt

View File

@ -1,6 +0,0 @@
<!-- Logo hand-made by Lonami (C) LonamiWebs 2018, tidied up by JuanPotato -->
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="45" fill="#3777b0" />
<path d="M20 30 h60 v10 l-2 2 h-17.5 l-10.5 43 l-2 2 l-12.5 -45 h-17.5 v-10" fill="#f0a727"/>
<path d="M20 30 h60 v10 h-17.5 l-12.5 45 l-12.5 -45 h-17.5 v-10" fill="#ffd750"/>
</svg>

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 0 B

View File

@ -1,4 +0,0 @@
cryptg
pysocks
hachoir3
pillow

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,93 +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, run:
.. code-block:: sh
pip3 install -U telethon --user
To install or upgrade the library to the latest version.
Installing Development Versions
===============================
If you want the *latest* unreleased changes,
you can run the following command instead:
.. code-block:: sh
pip3 install -U https://github.com/LonamiWebs/Telethon/archive/master.zip --user
.. 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:
.. 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,22 +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.

View File

@ -1,79 +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.sync import TelegramClient
# Remember to use your own values from my.telegram.org!
api_id = 12345
api_hash = '0123456789abcdef0123456789abcdef'
with TelegramClient('anon', api_id, api_hash) as client:
# Getting information about yourself
me = client.get_me()
# "me" is an 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:
for dialog in client.iter_dialogs():
print(dialog.name, 'has ID', dialog.id)
# You can send messages to yourself...
client.send_message('me', 'Hello, myself!')
# ...to some chat ID
client.send_message(-100123456, 'Hello, group!')
# ...to your contacts
client.send_message('+34600123123', 'Hello, friend!')
# ...or even to any username
client.send_message('TelethonChat', 'Hello, Telethon!')
# You can, of course, use markdown in your messages:
message = client.send_message(
'me',
'This message has **bold**, `code`, __italics__ and '
'a [nice website](https://lonamiwebs.github.io)!',
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
message.reply('Cool!')
# Or send files, songs, documents, albums...
client.send_file('me', '/home/me/Pictures/holidays.jpg')
# You can print the message history of any chat:
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 = message.download_media()
print('File saved to', path)
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.

View File

@ -1,179 +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.sync 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.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.
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 `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=(socks.SOCKS5, '127.0.0.1', 4444))
(of course, replacing the IP and port with the IP and port of the proxy).
The ``proxy=`` argument should be a tuple, a list or a dict,
consisting of parameters described `in PySocks usage`__.
.. __: 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) 5s/%(asctime)s] %(name)s: %(message)s',
level=logging.WARNING)
Getting Started
===============
Let's start things with an example to automate replies:
.. code-block:: python
from telethon import TelegramClient, events
client = TelegramClient('anon', api_id, api_hash)
@client.on(events.NewMessage)
async def my_event_handler(event):
if 'hello' in event.raw_text:
await event.reply('hi!')
client.start()
client.run_until_disconnected()
This code isn't much, but there might be some things unclear.
Let's break it down:
.. code-block:: python
from telethon import TelegramClient, events
client = TelegramClient('anon', api_id, api_hash)
This is normal creation (of course, pass session name, API ID and hash).
Nothing we don't know already.
.. code-block:: python
@client.on(events.NewMessage)
This Python decorator will attach itself to the ``my_event_handler``
definition, and basically means that *on* a `NewMessage
<telethon.events.newmessage.NewMessage>` *event*,
the callback function you're about to define will be called:
.. code-block:: python
async def my_event_handler(event):
if 'hello' in event.raw_text:
await event.reply('hi!')
If a `NewMessage
<telethon.events.newmessage.NewMessage>` event occurs,
and ``'hello'`` is in the text of the message, we `reply()
<telethon.tl.custom.message.Message.reply>` to the event
with a ``'hi!'`` message.
.. note::
Event handlers **must** be ``async def``. After all,
Telethon is an asynchronous library based on `asyncio`,
which is a safer and often faster approach to threads.
You **must** ``await`` all method calls that use
network requests, which is most of them.
More Examples
=============
Replying to messages with hello is fun, but, can we do more?
.. code-block:: python
@client.on(events.NewMessage(outgoing=True, pattern=r'\.save'))
async def handler(event):
if event.is_reply:
replied = await event.get_reply_message()
sender = replied.sender
await client.download_profile_photo(sender)
await event.respond('Saved your photo {}'.format(sender.username))
We could also get replies. This event filters outgoing messages
(only those that we send will trigger the method), then we filter
by the regex ``r'\.save'``, which will match messages starting
with ``".save"``.
Inside the method, we check whether the event is replying to another message
or not. If it is, we get the reply message and the sender of that message,
and download their profile photo.
Let's delete messages which contain "heck". We don't allow swearing here.
.. code-block:: python
@client.on(events.NewMessage(pattern=r'(?i).*heck'))
async def handler(event):
await event.delete()
With the ``r'(?i).*heck'`` regex, we match case-insensitive
"heck" anywhere in the message. Regex is very powerful and you
can learn more at https://regexone.com/.
So far, we have only seen the `NewMessage
<telethon.events.newmessage.NewMessage>`, but there are many more
which will be covered later. This is only a small introduction to updates.
Entities
========
When you need the user or chat where an event occurred, you **must** use
the following methods:
.. code-block:: python
async def handler(event):
# Good
chat = await event.get_chat()
sender = await event.get_sender()
chat_id = event.chat_id
sender_id = event.sender_id
# BAD. Don't do this
chat = event.chat
sender = event.sender
chat_id = event.chat.id
sender_id = event.sender.id
Events are like messages, but don't have all the information a message has!
When you manually get a message, it will have all the information it needs.
When you receive an update about a message, it **won't** have all the
information, so you have to **use the methods**, not the properties.
Make sure you understand the code seen here before continuing!
As a rule of thumb, remember that new message events behave just
like message objects, so you can do with them everything you can
do with a message object.

View File

@ -1,361 +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?
========================
.. code-block:: python
# First we need the asyncio library
import asyncio
# Then we need a loop to work with
loop = asyncio.get_event_loop()
# 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 need to run the loop with a task
loop.run_until_complete(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
import asyncio
loop = asyncio.get_event_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)
loop = asyncio.get_event_loop()
# ^ this assigns the default event loop from the main thread to a variable
loop.run_until_complete(main())
# ^ this runs the *entire* loop until the main() function finishes.
# While the main() function does not finish, the loop will be running.
# While the loop is running, you can't run it again.
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
loop = asyncio.get_event_loop() # get the default loop for the main thread
loop.create_task(world(2)) # create the world task, passing 2 as delay
loop.create_task(hello(delay=1)) # another task, but with delay 1
try:
# run the event loop forever; ctrl+c to stop it
# we could also run the loop for three seconds:
# loop.run_until_complete(asyncio.sleep(3))
loop.run_forever()
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')
loop = asyncio.get_event_loop()
loop.create_task(world(2))
loop.create_task(hello(1))
loop.run_until_complete(asyncio.sleep(3))
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. You
may want to create a loop in a new thread and make sure to pass it to
the client:
.. code-block:: python
import asyncio
import threading
def go():
loop = asyncio.new_event_loop()
client = TelegramClient(..., loop=loop)
...
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, and if you don't
pass a loop when creating the client, it uses ``asyncio.get_event_loop()``,
which only works in the main thread.
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('TelethonChat', 10),
client.send_message('TelethonOfftopic', 'Hey guys!'),
client.download_profile_photo('TelethonChat')
)
loop.run_until_complete(main())
This code will get the 10 last messages from `@TelethonChat
<https://t.me/TelethonChat>`_, send one to `@TelethonOfftopic
<https://t.me/TelethonOfftopic>`_, and also download the profile
photo of the main group. `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('TelethonChat', 10))
loop.create_task(client.send_message('TelethonOfftopic', 'Hey guys!'))
loop.create_task(client.download_profile_photo('TelethonChat'))
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://lonamiwebs.github.io/blog/asyncio/>`_ about `asyncio`, which
has some more examples and pictures to help you understand what happens
when the loop runs.

View File

@ -1,333 +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.
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 repository `HTTP Bot API vs MTProto comparison`_ 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 `echobot2.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, 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 less 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):
await 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
.. _HTTP Bot API vs MTProto comparison: https://github.com/telegram-mtproto/botapi-comparison
.. _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
.. _echobot2.py: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot2.py

View File

@ -1,308 +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
# 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 feels the entity cache.
dialogs = client.get_dialogs()
# All of these work and do the same.
lonami = client.get_entity('lonami')
lonami = client.get_entity('t.me/lonami')
lonami = client.get_entity('https://telegram.dog/lonami')
# Other kind of entities.
channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg')
contact = client.get_entity('+34xxxxxxxxx')
friend = client.get_entity(friend_id)
# Getting entities through their ID (User, Chat or Channel)
entity = client.get_entity(some_id)
# You can be more explicit about the type for said ID by wrapping
# it inside a Peer instance. This is recommended but not necessary.
from telethon.tl.types import PeerUser, PeerChat, PeerChannel
my_user = client.get_entity(PeerUser(some_id))
my_chat = client.get_entity(PeerChat(some_id))
my_channel = client.get_entity(PeerChannel(some_id))
.. 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('lonami', 'hi!')
<telethon.client.messages.MessageMethods.send_message>`
become possible.
Every entity the library encounters (in any response to any call) will by
default be cached in the ``.session`` file (an SQLite database), to avoid
performing unnecessary API calls. If the entity cannot be found, additonal
calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be
made to obtain the required information.
Entities vs. Input Entities
===========================
.. note::
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 use 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
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
message.get_chat()
# ...etc
`SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>` is similar:
.. code-block:: python
message.user_id
message.get_input_user()
message.user
# ...etc
Quite a few things implement them, so it makes sense to reuse the code.
For example, all events (except raw updates) implement `ChatGetter
<telethon.tl.custom.chatgetter.ChatGetter>` since all events occur
in some chat.
Summary
=======
TL;DR; If you're here because of *"Could not find the input entity for"*,
you must ask yourself "how did I find this entity through official
applications"? Now do the same with the library. Use what applies:
.. code-block:: python
with client:
# Does it have an username? Use it!
entity = client.get_entity(username)
# Do you have a conversation open with them? Get dialogs.
client.get_dialogs()
# Are they participant of some group? Get them.
client.get_participants('TelethonChat')
# Is the entity the original sender of a forwarded message? Get it.
client.get_messages('TelethonChat', 100)
# NOW you can use the ID, anywhere!
entity = client.get_entity(123456)
client.send_message(123456, 'Hi!')
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,77 +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). All the errors are
available in :ref:`telethon-errors`, but some examples are:
- ``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:
print(client.get_messages(chat)[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.
- ``CdnFileTamperedError``, if the media you were trying to download
from a CDN has been altered.
- ``ChatAdminRequiredError``, you don't have permissions to perform
said operation on a chat or channel. Try avoiding filters, i.e. when
searching messages.
The generic classes for different error codes are:
- ``InvalidDCError`` (303), the request must be repeated on another DC.
- ``BadRequestError`` (400), the request contained errors.
- ``UnauthorizedError`` (401), the user is not authorized yet.
- ``ForbiddenError`` (403), privacy violation error.
- ``NotFoundError`` (404), make sure you're invoking ``Request``\ 's!
If the error is not recognised, it will only be an ``RPCError``.
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))``).
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.

View File

@ -1,229 +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.
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. Whenever you need something, don't forget to `check the documentation`_
and look for the `method you need`_. There you can go through a sorted list
of everything you can do.
.. note::
The reason to keep both https://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.
.. important::
All the examples in this documentation assume that you have
``from telethon import sync`` or ``import telethon.sync`` for the
sake of simplicity and that you understand what it does (see
:ref:`compatibility-and-convenience` for more). Simply add
either line at the beginning of your project and it will work.
You should also refer to the documentation to see what the objects
(constructors) Telegram returns look like. Every constructor inherits
from a common type, and that's the reason for this distinction.
Say `client.send_message()
<telethon.client.messages.MessageMethods.send_message>` didn't exist,
we could `use the search`_ to look for "message". There we would find
:tl:`SendMessageRequest`, which we can work with.
Every request is a Python class, and has the parameters needed for you
to invoke it. You can also call ``help(request)`` for information on
what input parameters it takes. Remember to "Copy import to the
clipboard", or your script won't be aware of this class! Now we have:
.. code-block:: python
from telethon.tl.functions.messages import SendMessageRequest
If you're going to use a lot of these, you may do:
.. code-block:: python
from telethon.tl import types, functions
# We now have access to 'functions.messages.SendMessageRequest'
We see that this request must take at least two parameters, a ``peer``
of type :tl:`InputPeer`, and a ``message`` which is just a Python
``str``\ ing.
How can we retrieve this :tl:`InputPeer`? We have two options. We manually
construct one, for instance:
.. code-block:: python
from telethon.tl.types import InputPeerUser
peer = InputPeerUser(user_id, user_hash)
Or we call `client.get_input_entity()
<telethon.client.users.UserMethods.get_input_entity>`:
.. code-block:: python
import telethon.sync
peer = client.get_input_entity('someone')
When you're going to invoke an API method, most require you to pass an
:tl:`InputUser`, :tl:`InputChat`, or so on, this is why using
`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`
is more straightforward (and often immediate, if you've seen the user before,
know their ID, etc.). If you also **need** to have information about the whole
user, use `client.get_entity() <telethon.client.users.UserMethods.get_entity>`
instead:
.. code-block:: python
entity = client.get_entity('someone')
In the later case, when you use the entity, the library will cast it to
its "input" version for you. If you already have the complete user and
want to cache its input version so the library doesn't have to do this
every time its used, simply call `telethon.utils.get_input_peer`:
.. code-block:: python
from telethon import utils
peer = utils.get_input_peer(entity)
.. note::
Since ``v0.16.2`` this is further simplified. The ``Request`` itself
will call `client.get_input_entity
<telethon.client.users.UserMethods.get_input_entity>` for you when
required, but it's good to remember what's happening.
After this small parenthesis about `client.get_entity
<telethon.client.users.UserMethods.get_entity>` versus
`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`,
we have everything we need. To invoke our
request we do:
.. code-block:: python
result = client(SendMessageRequest(peer, 'Hello there!'))
Message sent! Of course, this is only an example. There are over 250
methods available as of layer 80, and you can use every single of them
as you wish. Remember to use the right types! To sum up:
.. code-block:: python
result = client(SendMessageRequest(
client.get_input_entity('username'), 'Hello there!'
))
This can further be simplified to:
.. code-block:: python
result = client(SendMessageRequest('username', 'Hello there!'))
# Or even
result = client(SendMessageRequest(PeerChannel(id), 'Hello there!'))
.. note::
Note that some requests have a "hash" parameter. This is **not**
your ``api_hash``! It likely isn't your self-user ``.access_hash`` either.
It's a special hash used by Telegram to only send a difference of new data
that you don't already have with that request, so you can leave it to 0,
and it should work (which means no hash is known yet).
For those requests having a "limit" parameter, you can often set it to
zero to signify "return default amount". This won't work for all of them
though, for instance, in "messages.search" it will actually return 0 items.
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
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:
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]
.. _check the documentation: https://tl.telethon.dev
.. _method you need: https://tl.telethon.dev/methods/index.html
.. _use the search: https://tl.telethon.dev/?q=message&redirect=no

View File

@ -1,158 +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.
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.
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 to do.
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.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,100 +0,0 @@
======================
String-based Debugging
======================
Debugging is *really* important. Telegram's API is really big and there
is 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
user = client.get_entity('Lonami')
print(user)
That will show a huge **string** similar to the following:
.. code-block:: python
User(id=10885151, is_self=False, contact=False, mutual_contact=False, deleted=False, bot=False, bot_chat_history=False, bot_nochats=False, verified=False, restricted=False, min=False, bot_inline_geo=False, access_hash=123456789012345678, first_name='Lonami', last_name=None, username='Lonami', phone=None, photo=UserProfilePhoto(photo_id=123456789012345678, photo_small=FileLocation(dc_id=4, volume_id=1234567890, local_id=1234567890, secret=123456789012345678), photo_big=FileLocation(dc_id=4, volume_id=1234567890, local_id=1234567890, secret=123456789012345678)), status=UserStatusOffline(was_online=datetime.datetime(2018, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc)), bot_info_version=None, restriction_reason=None, bot_inline_placeholder=None, lang_code=None)
That's a lot of text. But as you can see, all the properties are there.
So if you want the username you **don't use regex** or anything like
splitting ``str(user)`` to get what you want. You just access the
attribute you need:
.. code-block:: python
username = user.username
Can we get better than the shown string, though? Yes!
.. code-block:: python
print(user.stringify())
Will show a much better:
.. code-block:: python
User(
id=10885151,
is_self=False,
contact=False,
mutual_contact=False,
deleted=False,
bot=False,
bot_chat_history=False,
bot_nochats=False,
verified=False,
restricted=False,
min=False,
bot_inline_geo=False,
access_hash=123456789012345678,
first_name='Lonami',
last_name=None,
username='Lonami',
phone=None,
photo=UserProfilePhoto(
photo_id=123456789012345678,
photo_small=FileLocation(
dc_id=4,
volume_id=123456789,
local_id=123456789,
secret=-123456789012345678
),
photo_big=FileLocation(
dc_id=4,
volume_id=123456789,
local_id=123456789,
secret=123456789012345678
)
),
status=UserStatusOffline(
was_online=datetime.datetime(2018, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc)
),
bot_info_version=None,
restriction_reason=None,
bot_inline_placeholder=None,
lang_code=None
)
Now it's easy to see how we could get, for example,
the ``was_online`` time. It's inside ``status``:
.. code-block:: python
online_at = user.status.was_online
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(user.status, types.UserStatusOffline):
print(user.status.was_online)

View File

@ -1,229 +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
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
You could also run `client.disconnected
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>`
until it completed.
But if you don't want to ``await``, then you should know what you want
to be doing instead! What matters is that you shouldn't let your script
die. If you don't care about updates, you don't need any of this.
Notice that unlike `client.disconnected
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>`,
`client.run_until_disconnected
<telethon.client.updates.UpdateMethods.run_until_disconnected>` will
handle ``KeyboardInterrupt`` with you. This method is special and can
also be ran while the loop is running, so you can do this:
.. code-block:: python
async def main():
await client.run_until_disconnected()
loop.run_until_complete(main())
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,193 +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 = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'friendly'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
'collapse_navigation': True,
'display_version': True,
'navigation_depth': 3,
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
'**': [
'globaltoc.html',
'relations.html', # needs 'show_related': True theme option to display
'searchbox.html',
]
}
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'Telethondoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Telethon.tex', 'Telethon Documentation',
author, 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'telethon', 'Telethon Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Telethon', 'Telethon Documentation',
author, 'Telethon', 'One line description of project.',
'Miscellaneous'),
]

View File

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

View File

@ -1,73 +0,0 @@
===============================
Telegram API in Other Languages
===============================
Telethon was made for **Python**, and as far as I know, there is no
*exact* port to other languages. However, there *are* other
implementations made by awesome people (one needs to be awesome to
understand the official Telegram documentation) on several languages
(even more Python too), listed below:
C
*
Possibly the most well-known unofficial open source implementation out
there by `@vysheng <https://github.com/vysheng>`__,
`tgl <https://github.com/vysheng/tgl>`__, and its console client
`telegram-cli <https://github.com/vysheng/tg>`__. Latest development
has been moved to `BitBucket <https://bitbucket.org/vysheng/tdcli>`__.
C++
===
The newest (and official) library, written from scratch, is called
`tdlib <https://github.com/tdlib/td>`__ and is what the Telegram X
uses. You can find more information in the official documentation,
published `here <https://core.telegram.org/tdlib/docs/>`__.
JavaScript
==========
`@zerobias <https://github.com/zerobias>`__ is working on
`telegram-mtproto <https://github.com/zerobias/telegram-mtproto>`__,
a work-in-progress JavaScript library installable via
`npm <https://www.npmjs.com/>`__.
Kotlin
======
`Kotlogram <https://github.com/badoualy/kotlogram>`__ is a Telegram
implementation written in Kotlin (one of the
`official <https://blog.jetbrains.com/kotlin/2017/05/kotlin-on-android-now-official/>`__
languages for
`Android <https://developer.android.com/kotlin/index.html>`__) by
`@badoualy <https://github.com/badoualy>`__, currently as a beta
yet working.
PHP
===
A PHP implementation is also available thanks to
`@danog <https://github.com/danog>`__ and his
`MadelineProto <https://github.com/danog/MadelineProto>`__ project, with
a very nice `online
documentation <https://daniil.it/MadelineProto/API_docs/>`__ too.
Python
======
A fairly new (as of the end of 2017) Telegram library written from the
ground up in Python by
`@delivrance <https://github.com/delivrance>`__ and his
`Pyrogram <https://github.com/pyrogram/pyrogram>`__ library.
There isn't really a reason to pick it over Telethon and it'd be kinda
sad to see you go, but it would be nice to know what you miss from each
other library in either one so both can improve.
Rust
====
Yet another work-in-progress implementation, this time for Rust thanks
to `@JuanPotato <https://github.com/JuanPotato>`__ under the fancy
name of `Vail <https://github.com/JuanPotato/Vail>`__.

View File

@ -1,37 +0,0 @@
============
Test Servers
============
To run Telethon on a test server, use the following code:
.. code-block:: python
client = TelegramClient(None, api_id, api_hash)
client.session.set_dc(dc_id, '149.154.167.40', 80)
You can check your ``'test ip'`` on https://my.telegram.org.
You should set ``None`` session so to ensure you're generating a new
authorization key for it (it would fail if you used a session where you
had previously connected to another data center).
Note that port 443 might not work, so you can try with 80 instead.
Once you're connected, you'll likely be asked to either sign in or sign up.
Remember `anyone can access the phone you
choose <https://core.telegram.org/api/datacenter#testing-redirects>`__,
so don't store sensitive data here.
Valid phone numbers are ``99966XYYYY``, where ``X`` is the ``dc_id`` and
``YYYY`` is any number you want, for example, ``1234`` in ``dc_id = 2`` would
be ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated five
times, in this case, ``22222`` so we can hardcode that:
.. code-block:: python
client = TelegramClient(None, api_id, api_hash)
client.session.set_dc(2, '149.154.167.40', 80)
client.start(
phone='9996621234', code_callback=lambda: '22222'
)

View File

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

View File

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

View File

@ -1,258 +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
client(JoinChannelRequest(channel))
# In the same way, you can also leave such channel
from telethon.tl.functions.channels import LeaveChannelRequest
client(LeaveChannelRequest(input_channel))
For more on channels, check the `channels namespace`__.
__ https://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 = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg'))
Adding someone else to such chat or channel
===========================================
If you don't want to add yourself, maybe because you're already in,
you can always add someone else with the :tl:`AddChatUserRequest`, which
use is very straightforward, or :tl:`InviteToChannelRequest` for channels:
.. code-block:: python
# For normal chats
from telethon.tl.functions.messages import AddChatUserRequest
# Note that ``user_to_add`` is NOT the name of the parameter.
# It's the user you want to add (``user_id=user_to_add``).
client(AddChatUserRequest(
chat_id,
user_to_add,
fwd_limit=10 # Allow the user to see the 10 last messages
))
# For channels (which includes megagroups)
from telethon.tl.functions.channels import InviteToChannelRequest
client(InviteToChannelRequest(
channel,
[users_to_add]
))
Checking a link without joining
===============================
If you don't need to join but rather check whether it's a group or a
channel, you can use the :tl:`CheckChatInviteRequest`, which takes in
the hash of said channel or group.
Admin Permissions
=================
Giving or revoking admin permissions can be done with the :tl:`EditAdminRequest`:
.. code-block:: python
from telethon.tl.functions.channels import EditAdminRequest
from telethon.tl.types import ChatAdminRights
# You need both the channel and who to grant permissions
# They can either be channel/user or input channel/input user.
#
# ChatAdminRights is a list of granted permissions.
# Set to True those you want to give.
rights = ChatAdminRights(
post_messages=None,
add_admins=None,
invite_users=None,
change_info=True,
ban_users=None,
delete_messages=True,
pin_messages=True,
invite_link=None,
edit_messages=None
)
# Equivalent to:
# rights = ChatAdminRights(
# change_info=True,
# delete_messages=True,
# pin_messages=True
# )
# Once you have a ChatAdminRights, invoke it
client(EditAdminRequest(channel, user, rights))
# User will now be able to change group info, delete other people's
# messages and pin messages.
#
# In a normal chat, you should do this instead:
from telethon.tl.functions.messages import EditChatAdminRequest
client(EditChatAdminRequest(chat_id, user, is_admin=True))
.. note::
Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set all
parameters to ``True`` to give a user full permissions, as not all
permissions are related to both broadcast channels/megagroups.
E.g. trying to set ``post_messages=True`` in a megagroup will raise an
error. It is recommended to always use keyword arguments, and to set only
the permissions the user needs. If you don't need to change a permission,
it can be omitted (full list `here`__).
Restricting Users
=================
Similar to how you give or revoke admin permissions, you can edit the
banned rights of a user through :tl:`EditBannedRequest` and its parameter
:tl:`ChatBannedRights`:
.. code-block:: python
from telethon.tl.functions.channels import EditBannedRequest
from telethon.tl.types import ChatBannedRights
from datetime import datetime, timedelta
# Restricting a user for 7 days, only allowing view/send messages.
#
# Note that it's "reversed". You must set to ``True`` the permissions
# you want to REMOVE, and leave as ``None`` those you want to KEEP.
rights = ChatBannedRights(
until_date=timedelta(days=7),
view_messages=None,
send_messages=None,
send_media=True,
send_stickers=True,
send_gifs=True,
send_games=True,
send_inline=True,
embed_links=True
)
# The above is equivalent to
rights = ChatBannedRights(
until_date=datetime.now() + timedelta(days=7),
send_media=True,
send_stickers=True,
send_gifs=True,
send_games=True,
send_inline=True,
embed_links=True
)
client(EditBannedRequest(channel, user, rights))
You can use a `datetime.datetime` object for ``until_date=``,
a `datetime.timedelta` or even a Unix timestamp. Note that if you ban
someone for less than 30 seconds or for more than 366 days, Telegram
will consider the ban to actually last forever. This is officially
documented under https://core.telegram.org/bots/api#restrictchatmember.
Kicking a member
================
Telegram doesn't actually have a request to kick a user from a group.
Instead, you need to restrict them so they can't see messages. Any date
is enough:
.. code-block:: python
from telethon.tl.functions.channels import EditBannedRequest
from telethon.tl.types import ChatBannedRights
client(EditBannedRequest(
channel, user, ChatBannedRights(
until_date=None,
view_messages=True
)
))
__ https://github.com/Kyle2142
__ https://github.com/LonamiWebs/Telethon/issues/490
__ https://tl.telethon.dev/constructors/channel_admin_rights.html
Increasing View Count in a Channel
==================================
It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and
while I don't understand why so many people ask this, the solution is to
use :tl:`GetMessagesViewsRequest`, setting ``increment=True``:
.. code-block:: python
# Obtain `channel' through dialogs or through client.get_entity() or anyhow.
# Obtain `msg_ids' through `.get_messages()` or anyhow. Must be a list.
client(GetMessagesViewsRequest(
peer=channel,
id=msg_ids,
increment=True
))
Note that you can only do this **once or twice a day** per account,
running this in a loop will obviously not increase the views forever
unless you wait a day between each iteration. If you run it any sooner
than that, the views simply won't be increased.
__ https://github.com/LonamiWebs/Telethon/issues/233
__ https://github.com/LonamiWebs/Telethon/issues/305
__ https://github.com/LonamiWebs/Telethon/issues/409
__ https://github.com/LonamiWebs/Telethon/issues/447

View File

@ -1,85 +0,0 @@
.. _telethon_projects:
=======================
Projects using Telethon
=======================
This page lists some **interesting and useful** real world
examples showcasing what can be built with the library.
.. note::
Do you have an interesting project that uses the library or know of any
that's not listed here? Feel free to leave a comment at
`issue 744 <https://github.com/LonamiWebs/Telethon/issues/744>`_
so it can be included in the next revision of the documentation!
You can also advertise your bot and its features, in the issue, although
it should be a big project which can be useful for others before being
included here, so please don't feel offended if it can't be here!
.. _projects-telegram-export:
telethon_examples/
==================
`telethon_examples <https://github.com/LonamiWebs/Telethon/tree/master/telethon_examples>`_ /
`LonamiWebs' site <https://lonamiwebs.github.io>`_
This documentation is not the only place where you can find useful code
snippets using the library. The main repository also has a folder with
some cool examples (even a Tkinter GUI!) which you can download, edit
and run to learn and play with them.
@TelethonSnippets
=================
`@TelethonSnippets <https://t.me/TelethonSnippets>`_
You can find useful short snippets for Telethon here.
telegram-export
===============
`telegram-export <https://github.com/expectocode/telegram-export>`_ /
`expectocode's GitHub <https://github.com/expectocode>`_
A tool to download Telegram data (users, chats, messages, and media)
into a database (and display the saved data).
.. _projects-mautrix-telegram:
mautrix-telegram
================
`mautrix-telegram <https://github.com/tulir/mautrix-telegram>`_ /
`maunium's site <https://maunium.net/>`_
A Matrix-Telegram hybrid puppeting/relaybot bridge.
.. _projects-telegramtui:
TelegramTUI
===========
`TelegramTUI <https://github.com/bad-day/TelegramTUI>`_ /
`bad-day's GitHub <https://github.com/bad-day>`_
A Telegram client on your terminal.
tgcloud
=======
`tgcloud <https://github.com/SlavikMIPT/tgcloud>`_ /
`tgcloud's site <https://dev.tgcloud.xyz/>`_
Opensource Telegram based cloud storage.
tgmount
=======
`tgmount <https://github.com/nktknshn/tgmount>`_ /
`nktknshn's GitHub <https://github.com/nktknshn>`_
Mount Telegram dialogs and channels as a Virtual File System.

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 = client(GetFullUserRequest(user))
# or even
full = client(GetFullUserRequest('username'))
bio = full.about
See :tl:`UserFull` to know what other fields you can access.
Updating your name and/or bio
=============================
The first name, last name and bio (about) can all be changed with the same
request. Omitted fields won't change after invoking :tl:`UpdateProfile`:
.. code-block:: python
from telethon.tl.functions.account import UpdateProfileRequest
client(UpdateProfileRequest(
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
client(UpdateUsernameRequest('new_username'))
Updating your profile photo
===========================
The easiest way is to upload a new file and use that as the profile photo
through :tl:`UploadProfilePhoto`:
.. code-block:: python
from telethon.tl.functions.photos import UploadProfilePhotoRequest
client(UploadProfilePhotoRequest(
client.upload_file('/path/to/some/file')
)))

View File

@ -1,16 +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 :ref:`telethon_projects`.

View File

@ -1,45 +0,0 @@
=====================
Working with messages
=====================
.. note::
These examples assume you have read :ref:`full-api`.
.. contents::
Sending stickers
================
Stickers are nothing else than ``files``, and when you successfully retrieve
the stickers for a certain sticker set, all you will have are ``handles`` to
these files. Remember, the files Telegram holds on their servers can be
referenced through this pair of ID/hash (unique per user), and you need to
use this handle when sending a "document" message. This working example will
send yourself the very first sticker you have:
.. code-block:: python
# Get all the sticker sets this user has
from telethon.tl.functions.messages import GetAllStickersRequest
sticker_sets = client(GetAllStickersRequest(0))
# Choose a sticker set
from telethon.tl.functions.messages import GetStickerSetRequest
from telethon.tl.types import InputStickerSetID
sticker_set = sticker_sets.sets[0]
# Get the stickers for this sticker set
stickers = client(GetStickerSetRequest(
stickerset=InputStickerSetID(
id=sticker_set.id, access_hash=sticker_set.access_hash
)
))
# Stickers are nothing more than files, so send that
client.send_file('me', stickers.documents[0])
.. _issues: https://github.com/LonamiWebs/Telethon/issues/215

View File

@ -1,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/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
examples/projects-using-telethon
.. toctree::
:hidden:
:caption: Developing
developing/philosophy.rst
developing/test-servers.rst
developing/project-structure.rst
developing/coding-style.rst
developing/understanding-the-type-language.rst
developing/tips-for-porting-the-project.rst
developing/telegram-api-in-other-languages.rst
.. toctree::
:hidden:
:caption: Miscellaneous
misc/changelog
misc/wall-of-shame.rst
misc/compatibility-and-convenience
.. toctree::
:hidden:
:caption: Telethon Modules
modules/client
modules/events
modules/custom
modules/utils
modules/errors
modules/sessions
modules/network
modules/helpers

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,187 +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()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
The ``telethon.sync`` magic module simply wraps every method behind:
.. code-block:: python
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
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,65 +0,0 @@
=============
Wall of Shame
=============
This project has an
`issues <https://github.com/LonamiWebs/Telethon/issues>`__ section for
you to file **issues** whenever you encounter any when working with the
library. Said section is **not** for issues on *your* program but rather
issues with Telethon itself.
If you have not made the effort to 1. read through the docs and 2.
`look for the method you need <https://tl.telethon.dev/>`__,
you will end up on the `Wall of
Shame <https://github.com/LonamiWebs/Telethon/issues?q=is%3Aissue+label%3ARTFM+is%3Aclosed>`__,
i.e. all issues labeled
`"RTFM" <http://www.urbandictionary.com/define.php?term=RTFM>`__:
**rtfm**
Literally "Read The F--king Manual"; a term showing the
frustration of being bothered with questions so trivial that the asker
could have quickly figured out the answer on their own with minimal
effort, usually by reading readily-available documents. People who
say"RTFM!" might be considered rude, but the true rude ones are the
annoying people who take absolutely no self-responibility and expect to
have all the answers handed to them personally.
*"Damn, that's the twelveth time that somebody posted this question
to the messageboard today! RTFM, already!"*
*by Bill M. July 27, 2004*
If you have indeed read the docs, and have tried looking for the method,
and yet you didn't find what you need, **that's fine**. Telegram's API
can have some obscure names at times, and for this reason, there is a
`"question"
label <https://github.com/LonamiWebs/Telethon/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20label%3Aquestion%20>`__
with questions that are okay to ask. Just state what you've tried so
that we know you've made an effort, or you'll go to the Wall of Shame.
Of course, if the issue you're going to open is not even a question but
a real issue with the library (thankfully, most of the issues have been
that!), you won't end up here. Don't worry.
Current winner
--------------
The current winner is `issue
213 <https://github.com/LonamiWebs/Telethon/issues/213>`__:
**Issue:**
.. figure:: https://user-images.githubusercontent.com/6297805/29822978-9a9a6ef0-8ccd-11e7-9ec5-934ea0f57681.jpg
:alt: Winner issue
Winner issue
**Answer:**
.. figure:: https://user-images.githubusercontent.com/6297805/29822983-9d523402-8ccd-11e7-9fb1-5783740ee366.jpg
:alt: Winner issue answer
Winner issue answer

View File

@ -1,102 +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
import asyncio
from telethon import TelegramClient
async def main():
client = await TelegramClient(name, api_id, api_hash).start()
# Now you can use all client methods listed below, like for example...
await client.send_message('me', 'Hello to myself!')
asyncio.get_event_loop().run_until_complete(main())
You **don't** need to import these `AuthMethods`, `MessageMethods`, etc.
Together they are the `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,145 +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:
SenderGetter
============
.. automodule:: telethon.tl.custom.sendergetter
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,19 +0,0 @@
.. _telethon-errors:
==========
API Errors
==========
These are the base errors that Telegram's API may raise.
See :ref:`rpc-errors` for a more friendly explanation.
.. automodule:: telethon.errors.common
:members:
:undoc-members:
:show-inheritance:
.. automodule:: telethon.errors.rpcbaseerrors
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,65 +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.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,193 +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 an 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
sign_up
log_out
edit_2fa
Base
----
.. py:currentmodule:: telethon.client.telegrambaseclient.TelegramBaseClient
.. autosummary::
:nosignatures:
connect
disconnect
is_connected
disconnected
loop
Messages
--------
.. py:currentmodule:: telethon.client.messages.MessageMethods
.. autosummary::
:nosignatures:
send_message
edit_message
delete_messages
forward_messages
iter_messages
get_messages
pin_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
Dialogs
-------
.. py:currentmodule:: telethon.client.dialogs.DialogMethods
.. autosummary::
:nosignatures:
iter_dialogs
get_dialogs
archive
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
iter_admin_log
get_admin_log
iter_profile_photos
get_profile_photos
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
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,202 +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 whenever a user joins or leaves a chat, or a message is pinned.
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.
A lot of fields are attributes and not properties, so they
are not shown here. Please refer to its full documentation.
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
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
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,221 +0,0 @@
.. _faq:
===
FAQ
===
Let's start the quick references section with some useful tips to keep in
mind, with the hope that you will understand why certain things work the
way that they do.
.. contents::
Code without errors doesn't work
================================
Then it probably has errors, but you haven't enabled logging yet.
To enable logging, at the following code to the top of your main file:
.. code-block:: python
import logging
logging.basicConfig(format='[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s',
level=logging.WARNING)
You can change the logging level to be something different, from less to more information:
.. code-block:: python
level=logging.CRITICAL # won't show errors (same as disabled)
level=logging.ERROR # will only show errors that you didn't handle
level=logging.WARNING # will also show messages with medium severity, such as internal Telegram issues
level=logging.INFO # will also show informational messages, such as connection or disconnections
level=logging.DEBUG # will show a lot of output to help debugging issues in the library
See the official Python documentation for more information on logging_.
How can I except FloodWaitError?
================================
You can use all errors from the API by importing:
.. code-block:: python
from telethon import errors
And except them as such:
.. code-block:: python
try:
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
=====================================================
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, and to sign up with
these phones through an official application.
We have also had reports from Kazakhstan and China, where connecting
would fail. To solve these connection problems, you should use a proxy.
Telegram may also ban virtual (VoIP) phone numbers,
as again, they're likely to be used for spam.
If you want to check if your account has been limited,
simply send a private message to `@SpamBot`_ through Telegram itself.
You should notice this by getting errors like ``PeerFloodError``,
which means you're limited, for instance,
when sending a message to some accounts but not others.
For more discussion, please see `issue 297`_.
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 = client.get_me()
print(me.username)
# ^ we used the dot operator to access the username attribute
result = 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()
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 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.
.. _logging: https://docs.python.org/3/library/logging.html
.. _@SpamBot: https://t.me/SpamBot
.. _issue 297: https://github.com/LonamiWebs/Telethon/issues/297
.. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/master/telethon_examples#quart_loginpy

View File

@ -1,347 +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
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
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
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
text
request_location
request_phone
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 +0,0 @@
telethon

View File

@ -1,2 +0,0 @@
pyaes
rsa

227
setup.py
View File

@ -1,227 +0,0 @@
#!/usr/bin/env python3
"""A setuptools based setup module.
See:
https://packaging.python.org/en/latest/distributing.html
https://github.com/pypa/sampleproject
Extra supported commands are:
* gen, to generate the classes required for Telethon to run or docs
* pypi, to generate sdist, bdist_wheel, and push to PyPi
"""
import itertools
import json
import re
import shutil
from os import chdir
from pathlib import Path
from subprocess import run
from sys import argv
from setuptools import find_packages, setup
class TempWorkDir:
"""Switches the working directory to be the one on which this file lives,
while within the 'with' block.
"""
def __init__(self):
self.original = None
def __enter__(self):
self.original = Path('.')
chdir(str(Path(__file__).parent))
return self
def __exit__(self, *args):
chdir(str(self.original))
GENERATOR_DIR = Path('telethon_generator')
LIBRARY_DIR = Path('telethon')
ERRORS_IN = GENERATOR_DIR / 'data/errors.csv'
ERRORS_OUT = LIBRARY_DIR / 'errors/rpcerrorlist.py'
METHODS_IN = GENERATOR_DIR / 'data/methods.csv'
# Which raw API methods are covered by *friendly* methods in the client?
FRIENDLY_IN = GENERATOR_DIR / 'data/friendly.csv'
TLOBJECT_IN_TLS = [Path(x) for x in GENERATOR_DIR.glob('data/*.tl')]
TLOBJECT_OUT = LIBRARY_DIR / 'tl'
IMPORT_DEPTH = 2
DOCS_IN_RES = GENERATOR_DIR / 'data/html'
DOCS_OUT = Path('docs')
def generate(which, action='gen'):
from telethon_generator.parsers import\
parse_errors, parse_methods, parse_tl, find_layer
from telethon_generator.generators import\
generate_errors, generate_tlobjects, generate_docs, clean_tlobjects
layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS)))
errors = list(parse_errors(ERRORS_IN))
methods = list(parse_methods(METHODS_IN, FRIENDLY_IN, {e.str_code: e for e in errors}))
tlobjects = list(itertools.chain(*(
parse_tl(file, layer, methods) for file in TLOBJECT_IN_TLS)))
if not which:
which.extend(('tl', 'errors'))
clean = action == 'clean'
action = 'Cleaning' if clean else 'Generating'
if 'all' in which:
which.remove('all')
for x in ('tl', 'errors', 'docs'):
if x not in which:
which.append(x)
if 'tl' in which:
which.remove('tl')
print(action, 'TLObjects...')
if clean:
clean_tlobjects(TLOBJECT_OUT)
else:
generate_tlobjects(tlobjects, layer, IMPORT_DEPTH, TLOBJECT_OUT)
if 'errors' in which:
which.remove('errors')
print(action, 'RPCErrors...')
if clean:
if ERRORS_OUT.is_file():
ERRORS_OUT.unlink()
else:
with ERRORS_OUT.open('w') as file:
generate_errors(errors, file)
if 'docs' in which:
which.remove('docs')
print(action, 'documentation...')
if clean:
if DOCS_OUT.is_dir():
shutil.rmtree(str(DOCS_OUT))
else:
generate_docs(tlobjects, methods, layer, DOCS_IN_RES, DOCS_OUT)
if 'json' in which:
which.remove('json')
print(action, 'JSON schema...')
json_files = [x.with_suffix('.json') for x in TLOBJECT_IN_TLS]
if clean:
for file in json_files:
if file.is_file():
file.unlink()
else:
def gen_json(fin, fout):
meths = []
constructors = []
for tl in parse_tl(fin, layer):
if tl.is_function:
meths.append(tl.to_dict())
else:
constructors.append(tl.to_dict())
what = {'constructors': constructors, 'methods': meths}
with open(fout, 'w') as f:
json.dump(what, f, indent=2)
for fs in zip(TLOBJECT_IN_TLS, json_files):
gen_json(*fs)
if which:
print('The following items were not understood:', which)
print(' Consider using only "tl", "errors" and/or "docs".')
print(' Using only "clean" will clean them. "all" to act on all.')
print(' For instance "gen tl errors".')
def main():
if len(argv) >= 2 and argv[1] in ('gen', 'clean'):
generate(argv[2:], argv[1])
elif len(argv) >= 2 and argv[1] == 'pypi':
# (Re)generate the code to make sure we don't push without it
generate(['tl', 'errors'])
# Try importing the telethon module to assert it has no errors
try:
import telethon
except:
print('Packaging for PyPi aborted, importing the module failed.')
return
for x in ('build', 'dist', 'Telethon.egg-info'):
shutil.rmtree(x, ignore_errors=True)
run('python3 setup.py sdist', shell=True)
run('python3 setup.py bdist_wheel', shell=True)
run('twine upload dist/*', shell=True)
for x in ('build', 'dist', 'Telethon.egg-info'):
shutil.rmtree(x, ignore_errors=True)
else:
# e.g. install from GitHub
if GENERATOR_DIR.is_dir():
generate(['tl', 'errors'])
# Get the long description from the README file
with open('README.rst', 'r', encoding='utf-8') as f:
long_description = f.read()
with open('telethon/version.py', 'r', encoding='utf-8') as f:
version = re.search(r"^__version__\s*=\s*'(.*)'.*$",
f.read(), flags=re.MULTILINE).group(1)
setup(
name='Telethon',
version=version,
description="Full-featured Telegram client library for Python 3",
long_description=long_description,
url='https://github.com/LonamiWebs/Telethon',
download_url='https://github.com/LonamiWebs/Telethon/releases',
author='Lonami Exo',
author_email='totufals@hotmail.com',
license='MIT',
# See https://stackoverflow.com/a/40300957/4759433
# -> https://www.python.org/dev/peps/pep-0345/#requires-python
# -> http://setuptools.readthedocs.io/en/latest/setuptools.html
python_requires='>=3.5',
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[
# 3 - Alpha
# 4 - Beta
# 5 - Production/Stable
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Topic :: Communications :: Chat',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6'
],
keywords='telegram api chat client library messaging mtproto',
packages=find_packages(exclude=[
'telethon_*', 'run_tests.py', 'try_telethon.py'
]),
install_requires=['pyaes', 'rsa'],
extras_require={
'cryptg': ['cryptg']
}
)
if __name__ == '__main__':
with TempWorkDir(): # Could just use a try/finally but this is + reusable
main()

View File

@ -1,13 +0,0 @@
from .client.telegramclient import TelegramClient
from .network import connection
from .tl import types, functions, custom
from .tl.custom import Button
from . import version, events, utils, errors
__version__ = version.__version__
__all__ = [
'TelegramClient', 'Button',
'types', 'functions', 'custom', 'errors',
'events', 'utils', 'connection'
]

View File

@ -1,25 +0,0 @@
"""
This package defines clients as subclasses of others, and then a single
`telethon.client.telegramclient.TelegramClient` which is subclass of them
all to provide the final unified interface while the methods can live in
different subclasses to be more maintainable.
The ABC is `telethon.client.telegrambaseclient.TelegramBaseClient` and the
first implementor is `telethon.client.users.UserMethods`, since calling
requests require them to be resolved first, and that requires accessing
entities (users).
"""
from .telegrambaseclient import TelegramBaseClient
from .users import UserMethods # Required for everything
from .messageparse import MessageParseMethods # Required for messages
from .uploads import UploadMethods # Required for messages to send files
from .updates import UpdateMethods # Required for buttons (register callbacks)
from .buttons import ButtonMethods # Required for messages to use buttons
from .messages import MessageMethods
from .chats import ChatMethods
from .dialogs import DialogMethods
from .downloads import DownloadMethods
from .account import AccountMethods
from .auth import AuthMethods
from .bots import BotMethods
from .telegramclient import TelegramClient

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:
with client.takeout() as takeout:
client.get_messages('me') # normal call
takeout.get_messages('me') # wrapped through takeout (less limits)
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
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,638 +0,0 @@
import getpass
import inspect
import os
import sys
import typing
from .. import utils, helpers, errors, password as pwd_mod
from ..tl import types, functions
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
class AuthMethods:
# region Public methods
def start(
self: 'TelegramClient',
phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '),
password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '),
*,
bot_token: str = None,
force_sms: bool = False,
code_callback: typing.Callable[[], typing.Union[str, int]] = None,
first_name: str = 'New User',
last_name: str = '',
max_attempts: int = 3) -> 'TelegramClient':
"""
Starts the client (connects and logs in if necessary).
By default, this method will be interactive (asking for
user input if needed), and will handle 2FA if enabled too.
If the phone doesn't belong to an existing account (and will hence
`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.
If the event loop is already running, this method returns a
coroutine that you should await on your own code; otherwise
the loop is ran until said coroutine completes.
Arguments
phone (`str` | `int` | `callable`):
The phone (or callable without arguments to get it)
to which the code will be sent. If a bot-token-like
string is given, it will be used as such instead.
The argument may be a coroutine.
password (`str`, `callable`, optional):
The password for 2 Factor Authentication (2FA).
This is only required if it is enabled in your account.
The argument may be a coroutine.
bot_token (`str`):
Bot Token obtained by `@BotFather <https://t.me/BotFather>`_
to log in as a bot. Cannot be specified with ``phone`` (only
one of either allowed).
force_sms (`bool`, optional):
Whether to force sending the code request as SMS.
This only makes sense when signing in with a `phone`.
code_callback (`callable`, optional):
A callable that will be used to retrieve the Telegram
login code. Defaults to `input()`.
The argument may be a coroutine.
first_name (`str`, optional):
The first name to be used if signing up. This has no
effect if the account already exists and you sign in.
last_name (`str`, optional):
Similar to the first name, but for the last. Optional.
max_attempts (`int`, optional):
How many times the code/password callback should be
retried or switching between signing in and signing up.
Returns
This `TelegramClient`, so initialization
can be chained with ``.start()``.
Example
.. code-block:: python
client = TelegramClient('anon', api_id, api_hash)
# Starting as a bot account
client.start(bot_token=bot_token)
# Starting as an user account
client.start(phone)
# Please enter the code you received: 12345
# Please enter your password: *******
# (You are now logged in)
# Starting using a context manager (this calls start()):
with client:
pass
"""
if code_callback is None:
def code_callback():
return input('Please enter the code you received: ')
elif not callable(code_callback):
raise ValueError(
'The code_callback parameter needs to be a callable '
'function that returns the code you received by Telegram.'
)
if not phone and not bot_token:
raise ValueError('No phone number or bot token provided.')
if phone and bot_token and not callable(phone):
raise ValueError('Both a phone and a bot token provided, '
'must only provide one of either')
coro = self._start(
phone=phone,
password=password,
bot_token=bot_token,
force_sms=force_sms,
code_callback=code_callback,
first_name=first_name,
last_name=last_name,
max_attempts=max_attempts
)
return (
coro if self.loop.is_running()
else self.loop.run_until_complete(coro)
)
async def _start(
self, phone, password, bot_token, force_sms,
code_callback, first_name, last_name, max_attempts):
if not self.is_connected():
await self.connect()
if await self.is_user_authorized():
return self
if not bot_token:
# Turn the callable into a valid phone number (or bot token)
while callable(phone):
value = phone()
if inspect.isawaitable(value):
value = await value
if ':' in value:
# Bot tokens have 'user_id:access_hash' format
bot_token = value
break
phone = utils.parse_phone(value) or phone
if bot_token:
await self.sign_in(bot_token=bot_token)
return self
me = None
attempts = 0
two_step_detected = False
sent_code = await self.send_code_request(phone, force_sms=force_sms)
sign_up = not sent_code.phone_registered
while attempts < max_attempts:
try:
value = code_callback()
if inspect.isawaitable(value):
value = await value
# Since sign-in with no code works (it sends the code)
# we must double-check that here. Else we'll assume we
# logged in, and it will return None as the User.
if not value:
raise errors.PhoneCodeEmptyError(request=None)
if sign_up:
me = await self.sign_up(value, first_name, last_name)
else:
# Raises SessionPasswordNeededError if 2FA enabled
me = await self.sign_in(phone, code=value)
break
except errors.SessionPasswordNeededError:
two_step_detected = True
break
except errors.PhoneNumberOccupiedError:
sign_up = False
except errors.PhoneNumberUnoccupiedError:
sign_up = True
except (errors.PhoneCodeEmptyError,
errors.PhoneCodeExpiredError,
errors.PhoneCodeHashEmptyError,
errors.PhoneCodeInvalidError):
print('Invalid code. Please try again.', file=sys.stderr)
attempts += 1
else:
raise RuntimeError(
'{} consecutive sign-in attempts failed. Aborting'
.format(max_attempts)
)
if two_step_detected:
if not password:
raise ValueError(
"Two-step verification is enabled for this account. "
"Please provide the 'password' argument to 'start()'."
)
if callable(password):
for _ in range(max_attempts):
try:
value = password()
if inspect.isawaitable(value):
value = await value
me = await self.sign_in(phone=phone, password=value)
break
except errors.PasswordHashInvalidError:
print('Invalid password. Please try again',
file=sys.stderr)
else:
raise errors.PasswordHashInvalidError(request=None)
else:
me = await self.sign_in(phone=phone, password=password)
# We won't reach here if any step failed (exit by exception)
signed, name = 'Signed in successfully as', utils.get_display_name(me)
try:
print(signed, name)
except UnicodeEncodeError:
# Some terminals don't support certain characters
print(signed, name.encode('utf-8', errors='ignore')
.decode('ascii', errors='ignore'))
return self
def _parse_phone_and_hash(self, phone, phone_hash):
"""
Helper method to both parse and validate phone and its hash.
"""
phone = utils.parse_phone(phone) or self._phone
if not phone:
raise ValueError(
'Please make sure to call send_code_request first.'
)
phone_hash = phone_hash or self._phone_code_hash.get(phone, None)
if not phone_hash:
raise ValueError('You also need to provide a phone_code_hash.')
return phone, phone_hash
async def sign_in(
self: 'TelegramClient',
phone: str = None,
code: typing.Union[str, int] = None,
*,
password: str = None,
bot_token: str = None,
phone_code_hash: str = None) -> 'types.User':
"""
Logs in to Telegram to an existing user or bot account.
You should only use this if you are not authorized yet.
This method will send the code if it's not provided.
.. note::
In most cases, you should simply use `start()` and not this method.
Arguments
phone (`str` | `int`):
The phone to send the code to if no code was provided,
or to override the phone that was previously used with
these requests.
code (`str` | `int`):
The code that Telegram sent. Note that if you have sent this
code through the application itself it will immediately
expire. If you want to send the code, obfuscate it somehow.
If you're not doing any of this you can ignore this note.
password (`str`):
2FA password, should be used if a previous call raised
``SessionPasswordNeededError``.
bot_token (`str`):
Used to sign in as a bot. Not all requests will be available.
This should be the hash the `@BotFather <https://t.me/BotFather>`_
gave you.
phone_code_hash (`str`, optional):
The hash returned by `send_code_request`. This can be left as
``None`` to use the last hash known for the phone to be used.
Returns
The signed in user, or the information about
:meth:`send_code_request`.
Example
.. code-block:: python
phone = '+34 123 123 123'
client.sign_in(phone) # send code
code = input('enter code: ')
client.sign_in(phone, code)
"""
me = await self.get_me()
if me:
return me
if phone and not code and not password:
return await self.send_code_request(phone)
elif code:
phone, phone_code_hash = \
self._parse_phone_and_hash(phone, phone_code_hash)
# May raise PhoneCodeEmptyError, PhoneCodeExpiredError,
# PhoneCodeHashEmptyError or PhoneCodeInvalidError.
result = await self(functions.auth.SignInRequest(
phone, phone_code_hash, str(code)))
elif password:
pwd = await self(functions.account.GetPasswordRequest())
result = await self(functions.auth.CheckPasswordRequest(
pwd_mod.compute_check(pwd, password)
))
elif bot_token:
result = await self(functions.auth.ImportBotAuthorizationRequest(
flags=0, bot_auth_token=bot_token,
api_id=self.api_id, api_hash=self.api_hash
))
else:
raise ValueError(
'You must provide a phone and a code the first time, '
'and a password only if an RPCError was raised before.'
)
return 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':
"""
Signs up to Telegram as a new user account.
Use this if you don't have an account yet.
You must call `send_code_request` first.
**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.
Arguments
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.
phone (`str` | `int`, optional):
The phone to sign up. This will be the last phone used by
default (you normally don't need to set this).
phone_code_hash (`str`, optional):
The hash returned by `send_code_request`. This can be left as
``None`` to use the last hash known for the phone to be used.
Returns
The new created :tl:`User`.
Example
.. code-block:: python
phone = '+34 123 123 123'
client.send_code_request(phone)
code = input('enter code: ')
client.sign_up(code, first_name='Anna', last_name='Banana')
"""
me = await 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()
phone, phone_code_hash = \
self._parse_phone_and_hash(phone, phone_code_hash)
result = await self(functions.auth.SignUpRequest(
phone_number=phone,
phone_code_hash=phone_code_hash,
phone_code=str(code),
first_name=first_name,
last_name=last_name
))
if self._tos:
await self(
functions.help.AcceptTermsOfServiceRequest(self._tos.id))
return self._on_login(result.user)
def _on_login(self, user):
"""
Callback called whenever the login or sign up process completes.
Returns the input user parameter.
"""
self._bot = bool(user.bot)
self._self_input_peer = utils.get_input_peer(user, allow_self=False)
self._authorized = True
return user
async def send_code_request(
self: 'TelegramClient',
phone: str,
*,
force_sms: bool = False) -> 'types.auth.SentCode':
"""
Sends the Telegram code needed to login to the given phone number.
Arguments
phone (`str` | `int`):
The phone to which the code will be sent.
force_sms (`bool`, optional):
Whether to force sending as SMS.
Returns
An instance of :tl:`SentCode`.
Example
.. code-block:: python
phone = '+34 123 123 123'
sent = client.send_code_request(phone)
print(sent)
if sent.phone_registered:
print('This phone has an existing account registered')
else:
print('This phone does not have an account registered')
"""
result = None
phone = utils.parse_phone(phone) or self._phone
phone_hash = self._phone_code_hash.get(phone)
if not phone_hash:
try:
result = await self(functions.auth.SendCodeRequest(
phone, self.api_id, self.api_hash, types.CodeSettings()))
except errors.AuthRestartError:
return await self.send_code_request(phone, force_sms=force_sms)
self._tos = result.terms_of_service
self._phone_code_hash[phone] = phone_hash = result.phone_code_hash
else:
force_sms = True
self._phone = phone
if force_sms:
result = await self(
functions.auth.ResendCodeRequest(phone, phone_hash))
self._phone_code_hash[phone] = result.phone_code_hash
return result
async def log_out(self: 'TelegramClient') -> bool:
"""
Logs out Telegram and deletes the current ``*.session`` file.
Returns
``True`` if the operation was successful.
Example
.. code-block:: python
# Note: you will need to login again!
client.log_out()
"""
try:
await self(functions.auth.LogOutRequest())
except errors.RPCError:
return False
self._bot = None
self._self_input_peer = None
self._authorized = False
self._state_cache.reset()
await self.disconnect()
self.session.delete()
return True
async def edit_2fa(
self: 'TelegramClient',
current_password: str = None,
new_password: str = None,
*,
hint: str = '',
email: str = None,
email_code_callback: typing.Callable[[int], str] = None) -> bool:
"""
Changes the 2FA settings of the logged in user.
Review carefully the parameter explanations before using this method.
Note that this method may be *incredibly* slow depending on the
prime numbers that must be used during the process to make sure
that everything is safe.
Has no effect if both current and new password are omitted.
Arguments
current_password (`str`, optional):
The current password, to authorize changing to ``new_password``.
Must be set if changing existing 2FA settings.
Must **not** be set if 2FA is currently disabled.
Passing this by itself will remove 2FA (if correct).
new_password (`str`, optional):
The password to set as 2FA.
If 2FA was already enabled, ``current_password`` **must** be set.
Leaving this blank or ``None`` will remove the password.
hint (`str`, optional):
Hint to be displayed by Telegram when it asks for 2FA.
Leaving unspecified is highly discouraged.
Has no effect if ``new_password`` is not set.
email (`str`, optional):
Recovery and verification email. If present, you must also
set `email_code_callback`, else it raises ``ValueError``.
email_code_callback (`callable`, optional):
If an email is provided, a callback that returns the code sent
to it must also be set. This callback may be asynchronous.
It should return a string with the code. The length of the
code will be passed to the callback as an input parameter.
If the callback returns an invalid code, it will raise
``CodeInvalidError``.
Returns
``True`` if successful, ``False`` otherwise.
Example
.. code-block:: python
# Setting a password for your account which didn't have
client.edit_2fa(new_password='I_<3_Telethon')
# Removing the password
client.edit_2fa(current_password='I_<3_Telethon')
"""
if new_password is None and current_password is None:
return False
if email and not callable(email_code_callback):
raise ValueError('email present without email_code_callback')
pwd = await self(functions.account.GetPasswordRequest())
pwd.new_algo.salt1 += os.urandom(32)
assert isinstance(pwd, types.account.Password)
if not pwd.has_password and current_password:
current_password = None
if current_password:
password = pwd_mod.compute_check(pwd, current_password)
else:
password = types.InputCheckPasswordEmpty()
if new_password:
new_password_hash = pwd_mod.compute_digest(
pwd.new_algo, new_password)
else:
new_password_hash = b''
try:
await self(functions.account.UpdatePasswordSettingsRequest(
password=password,
new_settings=types.account.PasswordInputSettings(
new_algo=pwd.new_algo,
new_password_hash=new_password_hash,
hint=hint,
email=email,
new_secure_settings=None
)
))
except errors.EmailUnconfirmedError as e:
code = email_code_callback(e.code_length)
if inspect.isawaitable(code):
code = await code
code = str(code)
await self(functions.account.ConfirmPasswordEmailRequest(code))
return True
# endregion
# region with blocks
async def __aenter__(self):
return await self.start()
async def __aexit__(self, *args):
await self.disconnect()
__enter__ = helpers._sync_enter
__exit__ = helpers._sync_exit
# endregion

View File

@ -1,57 +0,0 @@
import typing
from .. import hints
from ..tl import types, functions, custom
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
class BotMethods:
async def inline_query(
self: 'TelegramClient',
bot: 'hints.EntityLike',
query: str,
*,
offset: str = None,
geo_point: 'types.GeoPoint' = None) -> custom.InlineResults:
"""
Makes an inline query to the specified bot (e.g. ``@vote New Poll``).
Arguments
bot (`entity`):
The bot entity to which the inline query should be made.
query (`str`):
The query that should be made to the bot.
offset (`str`, optional):
The string offset to use for the bot.
geo_point (:tl:`GeoPoint`, optional)
The geo point location information to send to the bot
for localised results. Available under some bots.
Returns
A list of `custom.InlineResult
<telethon.tl.custom.inlineresult.InlineResult>`.
Example
.. code-block:: python
# Make an inline query to @like
results = client.inline_query('like', 'Do you like Telethon?')
# Send the first result to some chat
message = results[0].click('TelethonOffTopic')
"""
bot = await self.get_input_entity(bot)
result = await self(functions.messages.GetInlineBotResultsRequest(
bot=bot,
peer=types.InputPeerEmpty(),
query=query,
offset=offset or '',
geo_point=geo_point
))
return custom.InlineResults(self, result)

View File

@ -1,95 +0,0 @@
import typing
from .. import utils, hints
from ..tl import types, custom
class ButtonMethods:
@staticmethod
def build_reply_markup(
buttons: 'typing.Optional[hints.MarkupLike]',
inline_only: bool = False) -> 'typing.Optional[types.TypeReplyMarkup]':
"""
Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for
the given buttons.
Does nothing if either no buttons are provided or the provided
argument is already a reply markup.
You should consider using this method if you are going to reuse
the markup very often. Otherwise, it is not necessary.
This method is **not** asynchronous (don't use ``await`` on it).
Arguments
buttons (`hints.MarkupLike`):
The button, list of buttons, array of buttons or markup
to convert into a markup.
inline_only (`bool`, optional):
Whether the buttons **must** be inline buttons only or not.
Example
.. code-block:: python
from telethon import Button
markup = client.build_reply_markup(Button.inline('hi'))
client.send_message('click me', buttons=markup)
"""
if buttons is None:
return None
try:
if buttons.SUBCLASS_OF_ID == 0xe2e10ef2:
return buttons # crc32(b'ReplyMarkup'):
except AttributeError:
pass
if not utils.is_list_like(buttons):
buttons = [[buttons]]
elif not utils.is_list_like(buttons[0]):
buttons = [buttons]
is_inline = False
is_normal = False
resize = None
single_use = None
selective = None
rows = []
for row in buttons:
current = []
for button in row:
if isinstance(button, custom.Button):
if button.resize is not None:
resize = button.resize
if button.single_use is not None:
single_use = button.single_use
if button.selective is not None:
selective = button.selective
button = button.button
elif isinstance(button, custom.MessageButton):
button = button.button
inline = custom.Button._is_inline(button)
is_inline |= inline
is_normal |= not inline
if button.SUBCLASS_OF_ID == 0xbad74a3:
# 0xbad74a3 == crc32(b'KeyboardButton')
current.append(button)
if current:
rows.append(types.KeyboardButtonRow(current))
if inline_only and is_normal:
raise ValueError('You cannot use non-inline buttons here')
elif is_inline == is_normal and is_normal:
raise ValueError('You cannot mix inline with normal buttons')
elif is_inline:
return types.ReplyInlineMarkup(rows)
# elif is_normal:
return types.ReplyKeyboardMarkup(
rows, resize=resize, single_use=single_use, selective=selective)

View File

@ -1,764 +0,0 @@
import asyncio
import itertools
import string
import typing
from .. import helpers, utils, hints
from ..requestiter import RequestIter
from ..tl import types, functions, custom
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
_MAX_PARTICIPANTS_CHUNK_SIZE = 200
_MAX_ADMIN_LOG_CHUNK_SIZE = 100
_MAX_PROFILE_PHOTO_CHUNK_SIZE = 100
class _ChatAction:
_str_mapping = {
'typing': types.SendMessageTypingAction(),
'contact': types.SendMessageChooseContactAction(),
'game': types.SendMessageGamePlayAction(),
'location': types.SendMessageGeoLocationAction(),
'record-audio': types.SendMessageRecordAudioAction(),
'record-voice': types.SendMessageRecordAudioAction(), # alias
'record-round': types.SendMessageRecordRoundAction(),
'record-video': types.SendMessageRecordVideoAction(),
'audio': types.SendMessageUploadAudioAction(1),
'voice': types.SendMessageUploadAudioAction(1), # alias
'round': types.SendMessageUploadRoundAction(1),
'video': types.SendMessageUploadVideoAction(1),
'photo': types.SendMessageUploadPhotoAction(1),
'document': types.SendMessageUploadDocumentAction(1),
'file': types.SendMessageUploadDocumentAction(1), # alias
'song': types.SendMessageUploadDocumentAction(1), # alias
'cancel': types.SendMessageCancelAction()
}
def __init__(self, client, chat, action, *, delay, auto_cancel):
self._client = client
self._chat = chat
self._action = action
self._delay = delay
self._auto_cancel = auto_cancel
self._request = None
self._task = None
self._running = False
async def __aenter__(self):
self._chat = await self._client.get_input_entity(self._chat)
# Since `self._action` is passed by reference we can avoid
# recreating the request all the time and still modify
# `self._action.progress` directly in `progress`.
self._request = functions.messages.SetTypingRequest(
self._chat, self._action)
self._running = True
self._task = self._client.loop.create_task(self._update())
async def __aexit__(self, *args):
self._running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
self._task = None
__enter__ = helpers._sync_enter
__exit__ = helpers._sync_exit
async def _update(self):
try:
while self._running:
await self._client(self._request)
await asyncio.sleep(self._delay)
except ConnectionError:
pass
except asyncio.CancelledError:
if self._auto_cancel:
await self._client(functions.messages.SetTypingRequest(
self._chat, types.SendMessageCancelAction()))
def progress(self, current, total):
if hasattr(self._action, 'progress'):
self._action.progress = 100 * round(current / total)
class _ParticipantsIter(RequestIter):
async def _init(self, entity, filter, search, aggressive):
if isinstance(filter, type):
if filter in (types.ChannelParticipantsBanned,
types.ChannelParticipantsKicked,
types.ChannelParticipantsSearch,
types.ChannelParticipantsContacts):
# These require a `q` parameter (support types for convenience)
filter = filter('')
else:
filter = filter()
entity = await self.client.get_input_entity(entity)
if search and (filter
or not isinstance(entity, types.InputPeerChannel)):
# We need to 'search' ourselves unless we have a PeerChannel
search = search.casefold()
self.filter_entity = lambda ent: (
search in utils.get_display_name(ent).casefold() or
search in (getattr(ent, 'username', None) or '').casefold()
)
else:
self.filter_entity = lambda ent: True
# Only used for channels, but we should always set the attribute
self.requests = []
if isinstance(entity, types.InputPeerChannel):
self.total = (await self.client(
functions.channels.GetFullChannelRequest(entity)
)).full_chat.participants_count
if self.limit <= 0:
raise StopAsyncIteration
self.seen = set()
if aggressive and not filter:
self.requests.extend(functions.channels.GetParticipantsRequest(
channel=entity,
filter=types.ChannelParticipantsSearch(x),
offset=0,
limit=_MAX_PARTICIPANTS_CHUNK_SIZE,
hash=0
) for x in (search or string.ascii_lowercase))
else:
self.requests.append(functions.channels.GetParticipantsRequest(
channel=entity,
filter=filter or types.ChannelParticipantsSearch(search),
offset=0,
limit=_MAX_PARTICIPANTS_CHUNK_SIZE,
hash=0
))
elif isinstance(entity, types.InputPeerChat):
full = await self.client(
functions.messages.GetFullChatRequest(entity.chat_id))
if not isinstance(
full.full_chat.participants, types.ChatParticipants):
# ChatParticipantsForbidden won't have ``.participants``
self.total = 0
raise StopAsyncIteration
self.total = len(full.full_chat.participants.participants)
users = {user.id: user for user in full.users}
for participant in full.full_chat.participants.participants:
user = users[participant.user_id]
if not self.filter_entity(user):
continue
user = users[participant.user_id]
user.participant = participant
self.buffer.append(user)
return True
else:
self.total = 1
if self.limit != 0:
user = await self.client.get_entity(entity)
if self.filter_entity(user):
user.participant = None
self.buffer.append(user)
return True
async def _load_next_chunk(self):
if not self.requests:
return True
# Only care about the limit for the first request
# (small amount of people, won't be aggressive).
#
# Most people won't care about getting exactly 12,345
# members so it doesn't really matter not to be 100%
# precise with being out of the offset/limit here.
self.requests[0].limit = min(
self.limit - self.requests[0].offset, _MAX_PARTICIPANTS_CHUNK_SIZE)
if self.requests[0].offset > self.limit:
return True
results = await self.client(self.requests)
for i in reversed(range(len(self.requests))):
participants = results[i]
if not participants.users:
self.requests.pop(i)
continue
self.requests[i].offset += len(participants.participants)
users = {user.id: user for user in participants.users}
for participant in participants.participants:
user = users[participant.user_id]
if not self.filter_entity(user) or user.id in self.seen:
continue
self.seen.add(participant.user_id)
user = users[participant.user_id]
user.participant = participant
self.buffer.append(user)
class _AdminLogIter(RequestIter):
async def _init(
self, entity, admins, search, min_id, max_id,
join, leave, invite, restrict, unrestrict, ban, unban,
promote, demote, info, settings, pinned, edit, delete
):
if any((join, leave, invite, restrict, unrestrict, ban, unban,
promote, demote, info, settings, pinned, edit, delete)):
events_filter = types.ChannelAdminLogEventsFilter(
join=join, leave=leave, invite=invite, ban=restrict,
unban=unrestrict, kick=ban, unkick=unban, promote=promote,
demote=demote, info=info, settings=settings, pinned=pinned,
edit=edit, delete=delete
)
else:
events_filter = None
self.entity = await self.client.get_input_entity(entity)
admin_list = []
if admins:
if not utils.is_list_like(admins):
admins = (admins,)
for admin in admins:
admin_list.append(await self.client.get_input_entity(admin))
self.request = functions.channels.GetAdminLogRequest(
self.entity, q=search or '', min_id=min_id, max_id=max_id,
limit=0, events_filter=events_filter, admins=admin_list or None
)
async def _load_next_chunk(self):
self.request.limit = min(self.left, _MAX_ADMIN_LOG_CHUNK_SIZE)
r = await self.client(self.request)
entities = {utils.get_peer_id(x): x
for x in itertools.chain(r.users, r.chats)}
self.request.max_id = min((e.id for e in r.events), default=0)
for ev in r.events:
if isinstance(ev.action,
types.ChannelAdminLogEventActionEditMessage):
ev.action.prev_message._finish_init(
self.client, entities, self.entity)
ev.action.new_message._finish_init(
self.client, entities, self.entity)
elif isinstance(ev.action,
types.ChannelAdminLogEventActionDeleteMessage):
ev.action.message._finish_init(
self.client, entities, self.entity)
self.buffer.append(custom.AdminLogEvent(ev, entities))
if len(r.events) < self.request.limit:
return True
class _ProfilePhotoIter(RequestIter):
async def _init(
self, entity, offset, max_id
):
entity = await self.client.get_input_entity(entity)
if isinstance(entity, (types.InputPeerUser, types.InputPeerSelf)):
self.request = functions.photos.GetUserPhotosRequest(
entity,
offset=offset,
max_id=max_id,
limit=1
)
else:
self.request = functions.messages.SearchRequest(
peer=entity,
q='',
filter=types.InputMessagesFilterChatPhotos(),
min_date=None,
max_date=None,
offset_id=0,
add_offset=offset,
limit=1,
max_id=max_id,
min_id=0,
hash=0
)
if self.limit == 0:
self.request.limit = 1
result = await self.client(self.request)
if isinstance(result, types.photos.Photos):
self.total = len(result.photos)
elif isinstance(result, types.messages.Messages):
self.total = len(result.messages)
else:
# Luckily both photosSlice and messages have a count for total
self.total = getattr(result, 'count', None)
async def _load_next_chunk(self):
self.request.limit = min(self.left, _MAX_PROFILE_PHOTO_CHUNK_SIZE)
result = await self.client(self.request)
if isinstance(result, types.photos.Photos):
self.buffer = result.photos
self.left = len(self.buffer)
self.total = len(self.buffer)
elif isinstance(result, types.messages.Messages):
self.buffer = [x.action.photo for x in result.messages
if isinstance(x.action, types.MessageActionChatEditPhoto)]
self.left = len(self.buffer)
self.total = len(self.buffer)
elif isinstance(result, types.photos.PhotosSlice):
self.buffer = result.photos
self.total = result.count
if len(self.buffer) < self.request.limit:
self.left = len(self.buffer)
else:
self.request.offset += len(result.photos)
else:
self.buffer = [x.action.photo for x in result.messages
if isinstance(x.action, types.MessageActionChatEditPhoto)]
self.total = getattr(result, 'count', None)
if len(result.messages) < self.request.limit:
self.left = len(self.buffer)
elif result.messages:
self.request.add_offset = 0
self.request.offset_id = result.messages[-1].id
class ChatMethods:
# region Public methods
def iter_participants(
self: 'TelegramClient',
entity: 'hints.EntityLike',
limit: float = None,
*,
search: str = '',
filter: 'types.TypeChannelParticipantsFilter' = None,
aggressive: bool = False) -> _ParticipantsIter:
"""
Iterator over the participants belonging to the specified chat.
Arguments
entity (`entity`):
The entity from which to retrieve the participants list.
limit (`int`):
Limits amount of participants fetched.
search (`str`, optional):
Look for participants with this string in name/username.
If ``aggressive is True``, the symbols from this string will
be used.
filter (:tl:`ChannelParticipantsFilter`, optional):
The filter to be used, if you want e.g. only admins
Note that you might not have permissions for some filter.
This has no effect for normal chats or users.
.. note::
The filter :tl:`ChannelParticipantsBanned` will return
*restricted* users. If you want *banned* users you should
use :tl:`ChannelParticipantsKicked` instead.
aggressive (`bool`, optional):
Aggressively looks for all participants in the chat.
This is useful for channels since 20 July 2018,
Telegram added a server-side limit where only the
first 200 members can be retrieved. With this flag
set, more than 200 will be often be retrieved.
This has no effect if a ``filter`` is given.
Yields
The :tl:`User` objects returned by :tl:`GetParticipantsRequest`
with an additional ``.participant`` attribute which is the
matched :tl:`ChannelParticipant` type for channels/megagroups
or :tl:`ChatParticipants` for normal chats.
Example
.. code-block:: python
# Show all user IDs in a chat
for user in client.iter_participants(chat):
print(user.id)
# Search by name
for user in client.iter_participants(chat, search='name'):
print(user.username)
# Filter by admins
from telethon.tl.types import ChannelParticipantsAdmins
for user in client.iter_participants(chat, filter=ChannelParticipantsAdmins):
print(user.first_name)
"""
return _ParticipantsIter(
self,
limit,
entity=entity,
filter=filter,
search=search,
aggressive=aggressive
)
async def get_participants(
self: 'TelegramClient',
*args,
**kwargs) -> 'hints.TotalList':
"""
Same as `iter_participants()`, but returns a
`TotalList <telethon.helpers.TotalList>` instead.
Example
.. code-block:: python
users = client.get_participants(chat)
print(users[0].first_name)
for user in users:
if user.username is not None:
print(user.username)
"""
return await self.iter_participants(*args, **kwargs).collect()
def iter_admin_log(
self: 'TelegramClient',
entity: 'hints.EntityLike',
limit: float = None,
*,
max_id: int = 0,
min_id: int = 0,
search: str = None,
admins: 'hints.EntitiesLike' = None,
join: bool = None,
leave: bool = None,
invite: bool = None,
restrict: bool = None,
unrestrict: bool = None,
ban: bool = None,
unban: bool = None,
promote: bool = None,
demote: bool = None,
info: bool = None,
settings: bool = None,
pinned: bool = None,
edit: bool = None,
delete: bool = None) -> _AdminLogIter:
"""
Iterator over the admin log for the specified channel.
Note that you must be an administrator of it to use this method.
If none of the filters are present (i.e. they all are ``None``),
*all* event types will be returned. If at least one of them is
``True``, only those that are true will be returned.
Arguments
entity (`entity`):
The channel entity from which to get its admin log.
limit (`int` | `None`, optional):
Number of events to be retrieved.
The limit may also be ``None``, which would eventually return
the whole history.
max_id (`int`):
All the events with a higher (newer) ID or equal to this will
be excluded.
min_id (`int`):
All the events with a lower (older) ID or equal to this will
be excluded.
search (`str`):
The string to be used as a search query.
admins (`entity` | `list`):
If present, the events will be filtered by these admins
(or single admin) and only those caused by them will be
returned.
join (`bool`):
If ``True``, events for when a user joined will be returned.
leave (`bool`):
If ``True``, events for when a user leaves will be returned.
invite (`bool`):
If ``True``, events for when a user joins through an invite
link will be returned.
restrict (`bool`):
If ``True``, events with partial restrictions will be
returned. This is what the API calls "ban".
unrestrict (`bool`):
If ``True``, events removing restrictions will be returned.
This is what the API calls "unban".
ban (`bool`):
If ``True``, events applying or removing all restrictions will
be returned. This is what the API calls "kick" (restricting
all permissions removed is a ban, which kicks the user).
unban (`bool`):
If ``True``, events removing all restrictions will be
returned. This is what the API calls "unkick".
promote (`bool`):
If ``True``, events with admin promotions will be returned.
demote (`bool`):
If ``True``, events with admin demotions will be returned.
info (`bool`):
If ``True``, events changing the group info will be returned.
settings (`bool`):
If ``True``, events changing the group settings will be
returned.
pinned (`bool`):
If ``True``, events of new pinned messages will be returned.
edit (`bool`):
If ``True``, events of message edits will be returned.
delete (`bool`):
If ``True``, events of message deletions will be returned.
Yields
Instances of `AdminLogEvent <telethon.tl.custom.adminlogevent.AdminLogEvent>`.
Example
.. code-block:: python
for event in client.iter_admin_log(channel):
if event.changed_title:
print('The title changed from', event.old, 'to', event.new)
"""
return _AdminLogIter(
self,
limit,
entity=entity,
admins=admins,
search=search,
min_id=min_id,
max_id=max_id,
join=join,
leave=leave,
invite=invite,
restrict=restrict,
unrestrict=unrestrict,
ban=ban,
unban=unban,
promote=promote,
demote=demote,
info=info,
settings=settings,
pinned=pinned,
edit=edit,
delete=delete
)
async def get_admin_log(
self: 'TelegramClient',
*args,
**kwargs) -> 'hints.TotalList':
"""
Same as `iter_admin_log()`, but returns a ``list`` instead.
Example
.. code-block:: python
# Get a list of deleted message events which said "heck"
events = client.get_admin_log(channel, search='heck', delete=True)
# Print the old message before it was deleted
print(events[0].old)
"""
return await self.iter_admin_log(*args, **kwargs).collect()
def iter_profile_photos(
self: 'TelegramClient',
entity: 'hints.EntityLike',
limit: int = None,
*,
offset: int = 0,
max_id: int = 0) -> _ProfilePhotoIter:
"""
Iterator over a user's profile photos or a chat's photos.
Arguments
entity (`entity`):
The entity from which to get the profile or chat photos.
limit (`int` | `None`, optional):
Number of photos to be retrieved.
The limit may also be ``None``, which would eventually all
the photos that are still available.
offset (`int`):
How many photos should be skipped before returning the first one.
max_id (`int`):
The maximum ID allowed when fetching photos.
Yields
Instances of :tl:`Photo`.
Example
.. code-block:: python
# Download all the profile photos of some user
for photo in client.iter_profile_photos(user):
client.download_media(photo)
"""
return _ProfilePhotoIter(
self,
limit,
entity=entity,
offset=offset,
max_id=max_id
)
async def get_profile_photos(
self: 'TelegramClient',
*args,
**kwargs) -> 'hints.TotalList':
"""
Same as `iter_profile_photos()`, but returns a
`TotalList <telethon.helpers.TotalList>` instead.
Example
.. code-block:: python
# Get the photos of a channel
photos = client.get_profile_photos(channel)
# Download the oldest photo
client.download_media(photos[-1])
"""
return await self.iter_profile_photos(*args, **kwargs).collect()
def action(
self: 'TelegramClient',
entity: 'hints.EntityLike',
action: 'typing.Union[str, types.TypeSendMessageAction]',
*,
delay: float = 4,
auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]':
"""
Returns a context-manager object to represent a "chat action".
Chat actions indicate things like "user is typing", "user is
uploading a photo", etc.
If the action is ``'cancel'``, you should just ``await`` the result,
since it makes no sense to use a context-manager for it.
See the example below for intended usage.
Arguments
entity (`entity`):
The entity where the action should be showed in.
action (`str` | :tl:`SendMessageAction`):
The action to show. You can either pass a instance of
:tl:`SendMessageAction` or better, a string used while:
* ``'typing'``: typing a text message.
* ``'contact'``: choosing a contact.
* ``'game'``: playing a game.
* ``'location'``: choosing a geo location.
* ``'record-audio'``: recording a voice note.
You may use ``'record-voice'`` as alias.
* ``'record-round'``: recording a round video.
* ``'record-video'``: recording a normal video.
* ``'audio'``: sending an audio file (voice note or song).
You may use ``'voice'`` and ``'song'`` as aliases.
* ``'round'``: uploading a round video.
* ``'video'``: uploading a video file.
* ``'photo'``: uploading a photo.
* ``'document'``: uploading a document file.
You may use ``'file'`` as alias.
* ``'cancel'``: cancel any pending action in this chat.
Invalid strings will raise a ``ValueError``.
delay (`int` | `float`):
The delay, in seconds, to wait between sending actions.
For example, if the delay is 5 and it takes 7 seconds to
do something, three requests will be made at 0s, 5s, and
7s to cancel the action.
auto_cancel (`bool`):
Whether the action should be cancelled once the context
manager exists or not. The default is ``True``, since
you don't want progress to be shown when it has already
completed.
Returns
Either a context-manager object or a coroutine.
Example
.. code-block:: python
# Type for 2 seconds, then send a message
async with client.action(chat, 'typing'):
await asyncio.sleep(2)
await client.send_message(chat, 'Hello world! I type slow ^^')
# Cancel any previous action
await client.action(chat, 'cancel')
# Upload a document, showing its progress (most clients ignore this)
async with client.action(chat, 'document') as action:
client.send_file(chat, zip_file, progress_callback=action.progress)
"""
if isinstance(action, str):
try:
action = _ChatAction._str_mapping[action.lower()]
except KeyError:
raise ValueError('No such action "{}"'.format(action)) from None
elif not isinstance(action, types.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21:
# 0x20b2cc21 = crc32(b'SendMessageAction')
if isinstance(action, type):
raise ValueError('You must pass an instance, not the class')
else:
raise ValueError('Cannot use {} as action'.format(action))
if isinstance(action, types.SendMessageCancelAction):
# ``SetTypingRequest.resolve`` will get input peer of ``entity``.
return self(functions.messages.SetTypingRequest(
entity, types.SendMessageCancelAction()))
return _ChatAction(
self, entity, action, delay=delay, auto_cancel=auto_cancel)
# endregion

View File

@ -1,507 +0,0 @@
import asyncio
import itertools
import typing
from .. import utils, hints
from ..requestiter import RequestIter
from ..tl import types, functions, custom
_MAX_CHUNK_SIZE = 100
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
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)}
messages = {}
for m in r.messages:
m._finish_init(self.client, entities, None)
messages[m.id] = m
for d in r.dialogs:
# We check the offset date here because Telegram may ignore it
if self.offset_date:
date = getattr(messages.get(
d.top_message, None), '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)
cd = custom.Dialog(self.client, d, entities, messages)
if cd.dialog.pts:
self.client._channel_pts[cd.id] = cd.dialog.pts
if not self.ignore_migrated or getattr(
cd.entity, 'migrated_to', None) is None:
self.buffer.append(cd)
if len(r.dialogs) < self.request.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.
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((
messages[d.top_message]
for d in reversed(r.dialogs)
if d.top_message in messages
), 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 =\
entities[utils.get_peer_id(r.dialogs[-1].peer)]
class _DraftsIter(RequestIter):
async def _init(self, **kwargs):
r = await self.client(functions.messages.GetAllDraftsRequest())
self.buffer.extend(custom.Draft._from_update(self.client, u)
for u in r.updates)
async def _load_next_chunk(self):
return []
class DialogMethods:
# region Public methods
def iter_dialogs(
self: 'TelegramClient',
limit: float = None,
*,
offset_date: 'hints.DateLike' = None,
offset_id: int = 0,
offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(),
ignore_pinned: bool = False,
ignore_migrated: bool = False,
folder: int = None,
archived: bool = None
) -> _DialogsIter:
"""
Iterator over the dialogs (open conversations/subscribed channels).
Arguments
limit (`int` | `None`):
How many dialogs to be retrieved as maximum. Can be set to
``None`` to retrieve all dialogs. Note that this may take
whole minutes if you have hundreds of dialogs, as Telegram
will tell the library to slow down through a
``FloodWaitError``.
offset_date (`datetime`, optional):
The offset date to be used.
offset_id (`int`, optional):
The message ID to be used as an offset.
offset_peer (:tl:`InputPeer`, optional):
The peer to be used as an offset.
ignore_pinned (`bool`, optional):
Whether pinned dialogs should be ignored or not.
When set to ``True``, these won't be yielded at all.
ignore_migrated (`bool`, optional):
Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel`
should be included or not. By default all the chats in your
dialogs are returned, but setting this to ``True`` will ignore
(i.e. skip) them in the same way official applications do.
folder (`int`, optional):
The folder from which the dialogs should be retrieved.
If left unspecified, all dialogs (including those from
folders) will be returned.
If set to ``0``, all dialogs that don't belong to any
folder will be returned.
If set to a folder number like ``1``, only those from
said folder will be returned.
By default Telegram assigns the folder ID ``1`` to
archived chats, so you should use that if you need
to fetch the archived dialogs.
archived (`bool`, optional):
Alias for `folder`. If unspecified, all will be returned,
``False`` implies ``folder=0`` and ``True`` implies ``folder=1``.
Yields
Instances of `Dialog <telethon.tl.custom.dialog.Dialog>`.
Example
.. code-block:: python
# Print all dialog IDs and the title, nicely formatted
for dialog in client.iter_dialogs():
print('{:>14}: {}'.format(dialog.id, dialog.title))
"""
if archived is not None:
folder = 1 if archived else 0
return _DialogsIter(
self,
limit,
offset_date=offset_date,
offset_id=offset_id,
offset_peer=offset_peer,
ignore_pinned=ignore_pinned,
ignore_migrated=ignore_migrated,
folder=folder
)
async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList':
"""
Same as `iter_dialogs()`, but returns a
`TotalList <telethon.helpers.TotalList>` instead.
Example
.. code-block:: python
# Get all open conversation, print the title of the first
dialogs = client.get_dialogs()
first = dialogs[0]
print(first.title)
# Use the dialog somewhere else
client.send_message(first, 'hi')
# Getting only non-archived dialogs (both equivalent)
non_archived = client.get_dialogs(folder=0)
non_archived = client.get_dialogs(archived=False)
# Getting only archived dialogs (both equivalent)
archived = client.get_dialogs(folder=1)
non_archived = client.get_dialogs(archived=True)
"""
return await self.iter_dialogs(*args, **kwargs).collect()
def iter_drafts(self: 'TelegramClient') -> _DraftsIter:
"""
Iterator over all open draft messages.
Yields
Instances of `Draft <telethon.tl.custom.draft.Draft>`.
Example
.. code-block:: python
# Clear all drafts
for draft in client.get_drafts():
draft.delete()
"""
# TODO Passing a limit here makes no sense
return _DraftsIter(self, None)
async def get_drafts(self: 'TelegramClient') -> 'hints.TotalList':
"""
Same as `iter_drafts()`, but returns a list instead.
Example
.. code-block:: python
# Get drafts, print the text of the first
drafts = client.get_drafts()
print(drafts[0].text)
"""
return await self.iter_drafts().collect()
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 = client.get_dialogs(5)
client.edit_folder(dialogs, 1)
# Un-archiving the third dialog (archiving to folder 0)
client.edit_folder(dialog[2], 0)
# Moving the first dialog to folder 0 and the second to 1
dialogs = client.get_dialogs(2)
client.edit_folder(dialogs, [0, 1])
# Un-archiving all dialogs
client.archive(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), loop=self.loop)
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).
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.
Returns
The :tl:`Updates` object that the request produces,
or nothing for private conversations.
Example
.. code-block:: python
# Deleting the first dialog
dialogs = client.get_dialogs(5)
client.delete_dialog(dialogs[0])
# Leaving a channel by username
client.delete_dialog('username')
"""
entity = await self.get_input_entity(entity)
if isinstance(entity, types.InputPeerChannel):
return await self(functions.channels.LeaveChannelRequest(entity))
if isinstance(entity, types.InputPeerChat):
result = await self(functions.messages.DeleteChatUserRequest(
entity.chat_id, types.InputUserSelf()))
else:
result = None
await self(functions.messages.DeleteHistoryRequest(entity, 0, revoke=revoke))
return result
def conversation(
self: 'TelegramClient',
entity: 'hints.EntityLike',
*,
timeout: float = 60,
total_timeout: float = None,
max_messages: int = 100,
exclusive: bool = True,
replies_are_responses: bool = True) -> custom.Conversation:
"""
Creates a `Conversation <telethon.tl.custom.conversation.Conversation>`
with the given entity.
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> Lonami
name = conv.get_response().raw_text
# <you> Thanks Lonami!
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

View File

@ -1,862 +0,0 @@
import datetime
import io
import os
import pathlib
import typing
from .. import utils, helpers, errors, hints
from ..requestiter import RequestIter
from ..tl import TLObject, types, functions
try:
import aiohttp
except ImportError:
aiohttp = None
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
# Chunk sizes for upload.getFile must be multiples of the smallest size
MIN_CHUNK_SIZE = 4096
MAX_CHUNK_SIZE = 512 * 1024
class _DirectDownloadIter(RequestIter):
async def _init(
self, file, dc_id, offset, stride, chunk_size, request_size, file_size
):
self.request = functions.upload.GetFileRequest(
file, offset=offset, limit=request_size)
self.total = file_size
self._stride = stride
self._chunk_size = chunk_size
self._last_part = None
self._exported = dc_id and self.client.session.dc_id != dc_id
if not self._exported:
# The used sender will also change if ``FileMigrateError`` occurs
self._sender = self.client._sender
else:
try:
self._sender = await self.client._borrow_exported_sender(dc_id)
except errors.DcIdInvalidError:
# Can't export a sender for the ID we are currently in
config = await self.client(functions.help.GetConfigRequest())
for option in config.dc_options:
if option.ip_address == self.client.session.server_address:
self.client.session.set_dc(
option.id, option.ip_address, option.port)
self.client.session.save()
break
# TODO Figure out why the session may have the wrong DC ID
self._sender = self.client._sender
self._exported = False
async def _load_next_chunk(self):
cur = await self._request()
self.buffer.append(cur)
if len(cur) < self.request.limit:
self.left = len(self.buffer)
await self.close()
else:
self.request.offset += self._stride
async def _request(self):
try:
result = await self._sender.send(self.request)
if isinstance(result, types.upload.FileCdnRedirect):
raise NotImplementedError # TODO Implement
else:
return result.bytes
except errors.FileMigrateError as e:
self.client._log[__name__].info('File lives in another DC')
self._sender = await self.client._borrow_exported_sender(e.new_dc)
self._exported = True
return await self._request()
async def close(self):
if not self._sender:
return
try:
if self._exported:
await self.client._return_exported_sender(self._sender)
elif self._sender != self.client._sender:
await self._sender.disconnect()
finally:
self._sender = None
async def __aenter__(self):
pass
async def __aexit__(self, *args):
await self.close()
__enter__ = helpers._sync_enter
__exit__ = helpers._sync_exit
class _GenericDownloadIter(_DirectDownloadIter):
async def _load_next_chunk(self, mask=MIN_CHUNK_SIZE - 1):
# 1. Fetch enough for one chunk
data = b''
# 1.1. ``bad`` is how much into the data we have we need to offset
bad = self.request.offset & mask
before = self.request.offset
# 1.2. We have to fetch from a valid offset, so remove that bad part
self.request.offset -= bad
done = False
while not done and len(data) - bad < self._chunk_size:
cur = await self._request()
self.request.offset += self.request.limit
data += cur
done = len(cur) < self.request.limit
# 1.3 Restore our last desired offset
self.request.offset = before
# 2. Fill the buffer with the data we have
# 2.1. Slicing ``bytes`` is expensive, yield ``memoryview`` instead
mem = memoryview(data)
# 2.2. The current chunk starts at ``bad`` offset into the data,
# and each new chunk is ``stride`` bytes apart of the other
for i in range(bad, len(data), self._stride):
self.buffer.append(mem[i:i + self._chunk_size])
# 2.3. We will yield this offset, so move to the next one
self.request.offset += self._stride
# 2.4. If we are in the last chunk, we will return the last partial data
if done:
self.left = len(self.buffer)
await self.close()
return
# 2.5. If we are not done, we can't return incomplete chunks.
if len(self.buffer[-1]) != self._chunk_size:
self._last_part = self.buffer.pop().tobytes()
# 3. Be careful with the offsets. Re-fetching a bit of data
# is fine, since it greatly simplifies things.
# TODO Try to not re-fetch data
self.request.offset -= self._stride
class DownloadMethods:
# region Public methods
async def download_profile_photo(
self: 'TelegramClient',
entity: 'hints.EntityLike',
file: 'hints.FileLike' = None,
*,
download_big: bool = True) -> typing.Optional[str]:
"""
Downloads the profile photo from the given user, chat or channel.
Arguments
entity (`entity`):
From who the photo will be downloaded.
.. note::
This method expects the full entity (which has the data
to download the photo), not an input variant.
It's possible that sometimes you can't fetch the entity
from its input (since you can get errors like
``ChannelPrivateError``) but you already have it through
another call, like getting a forwarded message from it.
file (`str` | `file`, optional):
The output file path, directory, or stream-like object.
If the path exists and is a file, it will be overwritten.
If file is the type `bytes`, it will be downloaded in-memory
as a bytestring (e.g. ``file=bytes``).
download_big (`bool`, optional):
Whether to use the big version of the available photos.
Returns
``None`` if no photo was provided, or if it was Empty. On success
the file path is returned since it may differ from the one given.
Example
.. code-block:: python
# Download your own profile photo
path = client.download_profile_photo('me')
print(path)
"""
# hex(crc32(x.encode('ascii'))) for x in
# ('User', 'Chat', 'UserFull', 'ChatFull')
ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697)
# ('InputPeer', 'InputUser', 'InputChannel')
INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd)
if not isinstance(entity, TLObject) or entity.SUBCLASS_OF_ID in INPUTS:
entity = await self.get_entity(entity)
thumb = -1 if download_big else 0
possible_names = []
if entity.SUBCLASS_OF_ID not in ENTITIES:
photo = entity
else:
if not hasattr(entity, 'photo'):
# Special case: may be a ChatFull with photo:Photo
# This is different from a normal UserProfilePhoto and Chat
if not hasattr(entity, 'chat_photo'):
return None
return await self._download_photo(
entity.chat_photo, file, date=None,
thumb=thumb, progress_callback=None
)
for attr in ('username', 'first_name', 'title'):
possible_names.append(getattr(entity, attr, None))
photo = entity.photo
if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)):
dc_id = photo.dc_id
which = photo.photo_big if download_big else photo.photo_small
loc = types.InputPeerPhotoFileLocation(
peer=await self.get_input_entity(entity),
local_id=which.local_id,
volume_id=which.volume_id,
big=download_big
)
else:
# It doesn't make any sense to check if `photo` can be used
# as input location, because then this method would be able
# to "download the profile photo of a message", i.e. its
# media which should be done with `download_media` instead.
return None
file = self._get_proper_filename(
file, 'profile_photo', '.jpg',
possible_names=possible_names
)
try:
result = await self.download_file(loc, file, dc_id=dc_id)
return result if file is bytes else file
except errors.LocationInvalidError:
# See issue #500, Android app fails as of v4.6.0 (1155).
# The fix seems to be using the full channel chat photo.
ie = await self.get_input_entity(entity)
if isinstance(ie, types.InputPeerChannel):
full = await self(functions.channels.GetFullChannelRequest(ie))
return await self._download_photo(
full.full_chat.chat_photo, file,
date=None, progress_callback=None,
thumb=thumb
)
else:
# Until there's a report for chats, no need to.
return None
async def download_media(
self: 'TelegramClient',
message: 'hints.MessageLike',
file: 'hints.FileLike' = None,
*,
thumb: 'hints.FileLike' = None,
progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]:
"""
Downloads the given media from a message object.
Note that if the download is too slow, you should consider installing
``cryptg`` (through ``pip install cryptg``) so that decrypting the
received data is done in C instead of Python (much faster).
See also `Message.download_media() <telethon.tl.custom.message.Message.download_media>`.
Arguments
message (`Message <telethon.tl.custom.message.Message>` | :tl:`Media`):
The media or message containing the media that will be downloaded.
file (`str` | `file`, optional):
The output file path, directory, or stream-like object.
If the path exists and is a file, it will be overwritten.
If file is the type `bytes`, it will be downloaded in-memory
as a bytestring (e.g. ``file=bytes``).
progress_callback (`callable`, optional):
A callback function accepting two parameters:
``(received bytes, total)``.
thumb (`int` | :tl:`PhotoSize`, optional):
Which thumbnail size from the document or photo to download,
instead of downloading the document or photo itself.
If it's specified but the file does not have a thumbnail,
this method will return ``None``.
The parameter should be an integer index between ``0`` and
``len(sizes)``. ``0`` will download the smallest thumbnail,
and ``len(sizes) - 1`` will download the largest thumbnail.
You can also use negative indices.
You can also pass the :tl:`PhotoSize` instance to use.
In short, use ``thumb=0`` if you want the smallest thumbnail
and ``thumb=-1`` if you want the largest thumbnail.
Returns
``None`` if no media was provided, or if it was Empty. On success
the file path is returned since it may differ from the one given.
Example
.. code-block:: python
path = client.download_media(message)
client.download_media(message, filename)
# or
path = message.download_media()
message.download_media(filename)
"""
# TODO This won't work for messageService
if isinstance(message, types.Message):
date = message.date
media = message.media
else:
date = datetime.datetime.now()
media = message
if isinstance(media, str):
media = utils.resolve_bot_file_id(media)
if isinstance(media, types.MessageMediaWebPage):
if isinstance(media.webpage, types.WebPage):
media = media.webpage.document or media.webpage.photo
if isinstance(media, (types.MessageMediaPhoto, types.Photo)):
return await self._download_photo(
media, file, date, thumb, progress_callback
)
elif isinstance(media, (types.MessageMediaDocument, types.Document)):
return await self._download_document(
media, file, date, thumb, progress_callback
)
elif isinstance(media, types.MessageMediaContact) and thumb is None:
return self._download_contact(
media, file
)
elif isinstance(media, (types.WebDocument, types.WebDocumentNoProxy)) and thumb is None:
return await self._download_web_document(
media, file, progress_callback
)
async def download_file(
self: 'TelegramClient',
input_location: 'hints.FileLike',
file: 'hints.OutFileLike' = None,
*,
part_size_kb: float = None,
file_size: int = None,
progress_callback: 'hints.ProgressCallback' = None,
dc_id: int = None) -> typing.Optional[bytes]:
"""
Low-level method to download files from their input location.
Arguments
input_location (:tl:`InputFileLocation`):
The file location from which the file will be downloaded.
See `telethon.utils.get_input_location` source for a complete
list of supported types.
file (`str` | `file`, optional):
The output file path, directory, or stream-like object.
If the path exists and is a file, it will be overwritten.
If the file path is ``None`` or ``bytes``, then the result
will be saved in memory and returned as `bytes`.
part_size_kb (`int`, optional):
Chunk size when downloading files. The larger, the less
requests will be made (up to 512KB maximum).
file_size (`int`, optional):
The file size that is about to be downloaded, if known.
Only used if ``progress_callback`` is specified.
progress_callback (`callable`, optional):
A callback function accepting two parameters:
``(downloaded bytes, total)``. Note that the
``total`` is the provided ``file_size``.
dc_id (`int`, optional):
The data center the library should connect to in order
to download the file. You shouldn't worry about this.
Example
.. code-block:: python
# Download a file and print its header
data = client.download_file(input_file, bytes)
print(data[:16])
"""
if not part_size_kb:
if not file_size:
part_size_kb = 64 # Reasonable default
else:
part_size_kb = utils.get_appropriated_part_size(file_size)
part_size = int(part_size_kb * 1024)
if part_size % MIN_CHUNK_SIZE != 0:
raise ValueError(
'The part size must be evenly divisible by 4096.')
in_memory = file is None or file is bytes
if in_memory:
f = io.BytesIO()
elif isinstance(file, str):
# Ensure that we'll be able to download the media
helpers.ensure_parent_dir_exists(file)
f = open(file, 'wb')
else:
f = file
try:
async for chunk in self.iter_download(
input_location, request_size=part_size, dc_id=dc_id):
f.write(chunk)
if progress_callback:
progress_callback(f.tell(), file_size)
f.flush()
if in_memory:
return f.getvalue()
finally:
if isinstance(file, str) or in_memory:
f.close()
def iter_download(
self: 'TelegramClient',
file: 'hints.FileLike',
*,
offset: int = 0,
stride: int = None,
limit: int = None,
chunk_size: int = None,
request_size: int = MAX_CHUNK_SIZE,
file_size: int = None,
dc_id: int = None
):
"""
Iterates over a file download, yielding chunks of the file.
This method can be used to stream files in a more convenient
way, since it offers more control (pausing, resuming, etc.)
.. note::
Using a value for `offset` or `stride` which is not a multiple
of the minimum allowed `request_size`, or if `chunk_size` is
different from `request_size`, the library will need to do a
bit more work to fetch the data in the way you intend it to.
You normally shouldn't worry about this.
Arguments
file (`hints.FileLike`):
The file of which contents you want to iterate over.
offset (`int`, optional):
The offset in bytes into the file from where the
download should start. For example, if a file is
1024KB long and you just want the last 512KB, you
would use ``offset=512 * 1024``.
stride (`int`, optional):
The stride of each chunk (how much the offset should
advance between reading each chunk). This parameter
should only be used for more advanced use cases.
It must be bigger than or equal to the `chunk_size`.
limit (`int`, optional):
The limit for how many *chunks* will be yielded at most.
chunk_size (`int`, optional):
The maximum size of the chunks that will be yielded.
Note that the last chunk may be less than this value.
By default, it equals to `request_size`.
request_size (`int`, optional):
How many bytes will be requested to Telegram when more
data is required. By default, as many bytes as possible
are requested. If you would like to request data in
smaller sizes, adjust this parameter.
Note that values outside the valid range will be clamped,
and the final value will also be a multiple of the minimum
allowed size.
file_size (`int`, optional):
If the file size is known beforehand, you should set
this parameter to said value. Depending on the type of
the input file passed, this may be set automatically.
dc_id (`int`, optional):
The data center the library should connect to in order
to download the file. You shouldn't worry about this.
Yields
``bytes`` objects representing the chunks of the file if the
right conditions are met, or ``memoryview`` objects instead.
Example
.. code-block:: python
# Streaming `media` to an output file
# After the iteration ends, the sender is cleaned up
with open('photo.jpg', 'wb') as fd:
for chunk client.iter_download(media):
fd.write(chunk)
# Fetching only the header of a file (32 bytes)
# You should manually close the iterator in this case.
stream = client.iter_download(media, request_size=32)
header = next(stream)
stream.close()
assert len(header) == 32
# Fetching only the header, inside of an ``async def``
async def main():
stream = client.iter_download(media, request_size=32)
header = await stream.__anext__()
await stream.close()
assert len(header) == 32
"""
if chunk_size is None:
chunk_size = request_size
if limit is None and file_size is not None:
limit = (file_size + chunk_size - 1) // chunk_size
if stride is None:
stride = chunk_size
elif stride < chunk_size:
raise ValueError('stride must be >= chunk_size')
request_size -= request_size % MIN_CHUNK_SIZE
if request_size < MIN_CHUNK_SIZE:
request_size = MIN_CHUNK_SIZE
elif request_size > MAX_CHUNK_SIZE:
request_size = MAX_CHUNK_SIZE
old_dc = dc_id
dc_id, file = utils.get_input_location(file)
if dc_id is None:
dc_id = old_dc
if chunk_size == request_size \
and offset % MIN_CHUNK_SIZE == 0 \
and stride % MIN_CHUNK_SIZE == 0:
cls = _DirectDownloadIter
self._log[__name__].info('Starting direct file download in chunks of '
'%d at %d, stride %d', request_size, offset, stride)
else:
cls = _GenericDownloadIter
self._log[__name__].info('Starting indirect file download in chunks of '
'%d at %d, stride %d', request_size, offset, stride)
return cls(
self,
limit,
file=file,
dc_id=dc_id,
offset=offset,
stride=stride,
chunk_size=chunk_size,
request_size=request_size,
file_size=file_size
)
# endregion
# region Private methods
@staticmethod
def _get_thumb(thumbs, thumb):
if thumb is None:
return thumbs[-1]
elif isinstance(thumb, int):
return thumbs[thumb]
elif isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize,
types.PhotoStrippedSize)):
return thumb
else:
return None
def _download_cached_photo_size(self: 'TelegramClient', size, file):
# No need to download anything, simply write the bytes
if isinstance(size, types.PhotoStrippedSize):
data = utils.stripped_photo_to_jpg(size.bytes)
else:
data = size.bytes
if file is bytes:
return data
elif isinstance(file, str):
helpers.ensure_parent_dir_exists(file)
f = open(file, 'wb')
else:
f = file
try:
f.write(data)
finally:
if isinstance(file, str):
f.close()
return file
async def _download_photo(self: 'TelegramClient', photo, file, date, thumb, progress_callback):
"""Specialized version of .download_media() for photos"""
# Determine the photo and its largest size
if isinstance(photo, types.MessageMediaPhoto):
photo = photo.photo
if not isinstance(photo, types.Photo):
return
size = self._get_thumb(photo.sizes, thumb)
if not size or isinstance(size, types.PhotoSizeEmpty):
return
file = self._get_proper_filename(file, 'photo', '.jpg', date=date)
if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)):
return self._download_cached_photo_size(size, file)
result = await self.download_file(
types.InputPhotoFileLocation(
id=photo.id,
access_hash=photo.access_hash,
file_reference=photo.file_reference,
thumb_size=size.type
),
file,
file_size=size.size,
progress_callback=progress_callback
)
return result if file is bytes else file
@staticmethod
def _get_kind_and_names(attributes):
"""Gets kind and possible names for :tl:`DocumentAttribute`."""
kind = 'document'
possible_names = []
for attr in attributes:
if isinstance(attr, types.DocumentAttributeFilename):
possible_names.insert(0, attr.file_name)
elif isinstance(attr, types.DocumentAttributeAudio):
kind = 'audio'
if attr.performer and attr.title:
possible_names.append('{} - {}'.format(
attr.performer, attr.title
))
elif attr.performer:
possible_names.append(attr.performer)
elif attr.title:
possible_names.append(attr.title)
elif attr.voice:
kind = 'voice'
return kind, possible_names
async def _download_document(
self, document, file, date, thumb, progress_callback):
"""Specialized version of .download_media() for documents."""
if isinstance(document, types.MessageMediaDocument):
document = document.document
if not isinstance(document, types.Document):
return
kind, possible_names = self._get_kind_and_names(document.attributes)
file = self._get_proper_filename(
file, kind, utils.get_extension(document),
date=date, possible_names=possible_names
)
if thumb is None:
size = None
else:
size = self._get_thumb(document.thumbs, thumb)
if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)):
return self._download_cached_photo_size(size, file)
result = await self.download_file(
types.InputDocumentFileLocation(
id=document.id,
access_hash=document.access_hash,
file_reference=document.file_reference,
thumb_size=size.type if size else ''
),
file,
file_size=size.size if size else document.size,
progress_callback=progress_callback
)
return result if file is bytes else file
@classmethod
def _download_contact(cls, mm_contact, file):
"""
Specialized version of .download_media() for contacts.
Will make use of the vCard 4.0 format.
"""
first_name = mm_contact.first_name
last_name = mm_contact.last_name
phone_number = mm_contact.phone_number
# Remove these pesky characters
first_name = first_name.replace(';', '')
last_name = (last_name or '').replace(';', '')
result = (
'BEGIN:VCARD\n'
'VERSION:4.0\n'
'N:{f};{l};;;\n'
'FN:{f} {l}\n'
'TEL;TYPE=cell;VALUE=uri:tel:+{p}\n'
'END:VCARD\n'
).format(f=first_name, l=last_name, p=phone_number).encode('utf-8')
if file is bytes:
return result
elif isinstance(file, str):
file = cls._get_proper_filename(
file, 'contact', '.vcard',
possible_names=[first_name, phone_number, last_name]
)
f = open(file, 'wb')
else:
f = file
try:
f.write(result)
finally:
# Only close the stream if we opened it
if isinstance(file, str):
f.close()
return file
@classmethod
async def _download_web_document(cls, web, file, progress_callback):
"""
Specialized version of .download_media() for web documents.
"""
if not aiohttp:
raise ValueError(
'Cannot download web documents without the aiohttp '
'dependency install it (pip install aiohttp)'
)
# TODO Better way to get opened handles of files and auto-close
in_memory = file is bytes
if in_memory:
f = io.BytesIO()
elif isinstance(file, str):
kind, possible_names = cls._get_kind_and_names(web.attributes)
file = cls._get_proper_filename(
file, kind, utils.get_extension(web),
possible_names=possible_names
)
f = open(file, 'wb')
else:
f = file
try:
with aiohttp.ClientSession() as session:
# TODO Use progress_callback; get content length from response
# https://github.com/telegramdesktop/tdesktop/blob/c7e773dd9aeba94e2be48c032edc9a78bb50234e/Telegram/SourceFiles/ui/images.cpp#L1318-L1319
async with session.get(web.url) as response:
while True:
chunk = await response.content.read(128 * 1024)
if not chunk:
break
f.write(chunk)
finally:
if isinstance(file, str) or file is bytes:
f.close()
return f.getvalue() if in_memory else file
@staticmethod
def _get_proper_filename(file, kind, extension,
date=None, possible_names=None):
"""Gets a proper filename for 'file', if this is a path.
'kind' should be the kind of the output file (photo, document...)
'extension' should be the extension to be added to the file if
the filename doesn't have any yet
'date' should be when this file was originally sent, if known
'possible_names' should be an ordered list of possible names
If no modification is made to the path, any existing file
will be overwritten.
If any modification is made to the path, this method will
ensure that no existing file will be overwritten.
"""
if isinstance(file, pathlib.Path):
file = str(file.absolute())
if file is not None and not isinstance(file, str):
# Probably a stream-like object, we cannot set a filename here
return file
if file is None:
file = ''
elif os.path.isfile(file):
# Make no modifications to valid existing paths
return file
if os.path.isdir(file) or not file:
try:
name = None if possible_names is None else next(
x for x in possible_names if x
)
except StopIteration:
name = None
if not name:
if not date:
date = datetime.datetime.now()
name = '{}_{}-{:02}-{:02}_{:02}-{:02}-{:02}'.format(
kind,
date.year, date.month, date.day,
date.hour, date.minute, date.second,
)
file = os.path.join(file, name)
directory, name = os.path.split(file)
name, ext = os.path.splitext(name)
if not ext:
ext = extension
result = os.path.join(directory, name + ext)
if not os.path.isfile(result):
return result
i = 1
while True:
result = os.path.join(directory, '{} ({}){}'.format(name, i, ext))
if not os.path.isfile(result):
return result
i += 1
# endregion

View File

@ -1,172 +0,0 @@
import itertools
import re
import typing
from .. import utils
from ..tl import types
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
class MessageParseMethods:
# region Public properties
@property
def parse_mode(self: 'TelegramClient'):
"""
This property is the default parse mode used when sending messages.
Defaults to `telethon.extensions.markdown`. It will always
be either ``None`` or an object with ``parse`` and ``unparse``
methods.
When setting a different value it should be one of:
* Object with ``parse`` and ``unparse`` methods.
* A ``callable`` to act as the parse method.
* A ``str`` indicating the ``parse_mode``. For Markdown ``'md'``
or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'``
may be used.
The ``parse`` method should be a function accepting a single
parameter, the text to parse, and returning a tuple consisting
of ``(parsed message str, [MessageEntity instances])``.
The ``unparse`` method should be the inverse of ``parse`` such
that ``assert text == unparse(*parse(text))``.
See :tl:`MessageEntity` for allowed message entities.
Example
.. code-block:: python
# Disabling default formatting
client.parse_mode = None
# Enabling HTML as the default format
client.parse_mode = 'html'
"""
return self._parse_mode
@parse_mode.setter
def parse_mode(self: 'TelegramClient', mode: str):
self._parse_mode = utils.sanitize_parse_mode(mode)
# endregion
# region Private methods
async def _replace_with_mention(self: 'TelegramClient', entities, i, user):
"""
Helper method to replace ``entities[i]`` to mention ``user``,
or do nothing if it can't be found.
"""
try:
entities[i] = types.InputMessageEntityMentionName(
entities[i].offset, entities[i].length,
await self.get_input_entity(user)
)
return True
except (ValueError, TypeError):
return False
async def _parse_message_text(self: 'TelegramClient', message, parse_mode):
"""
Returns a (parsed message, entities) tuple depending on ``parse_mode``.
"""
if parse_mode is ():
parse_mode = self._parse_mode
else:
parse_mode = utils.sanitize_parse_mode(parse_mode)
if not parse_mode:
return message, []
message, msg_entities = parse_mode.parse(message)
for i in reversed(range(len(msg_entities))):
e = msg_entities[i]
if isinstance(e, types.MessageEntityTextUrl):
m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url)
if m:
user = int(m.group(1)) if m.group(1) else e.url
is_mention = await self._replace_with_mention(msg_entities, i, user)
if not is_mention:
del msg_entities[i]
elif isinstance(e, (types.MessageEntityMentionName,
types.InputMessageEntityMentionName)):
is_mention = await self._replace_with_mention(msg_entities, i, e.user_id)
if not is_mention:
del msg_entities[i]
return message, msg_entities
def _get_response_message(self: 'TelegramClient', request, result, input_chat):
"""
Extracts the response message known a request and Update result.
The request may also be the ID of the message to match.
If ``request is None`` this method returns ``{id: message}``.
If ``request.random_id`` is a list, this method returns a list too.
"""
if isinstance(result, types.UpdateShort):
updates = [result.update]
entities = {}
elif isinstance(result, (types.Updates, types.UpdatesCombined)):
updates = result.updates
entities = {utils.get_peer_id(x): x
for x in
itertools.chain(result.users, result.chats)}
else:
return None
random_to_id = {}
id_to_message = {}
for update in updates:
if isinstance(update, types.UpdateMessageID):
random_to_id[update.random_id] = update.id
elif isinstance(update, (
types.UpdateNewChannelMessage, types.UpdateNewMessage)):
update.message._finish_init(self, entities, input_chat)
id_to_message[update.message.id] = update.message
elif (isinstance(update, types.UpdateEditMessage)
and not isinstance(request.peer, types.InputPeerChannel)):
if request.id == update.message.id:
update.message._finish_init(self, entities, input_chat)
return update.message
elif (isinstance(update, types.UpdateEditChannelMessage)
and utils.get_peer_id(request.peer) ==
utils.get_peer_id(update.message.to_id)):
if request.id == update.message.id:
update.message._finish_init(self, entities, input_chat)
return update.message
if request is None:
return id_to_message
random_id = request if isinstance(request, int) else request.random_id
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.
self._log[__name__].warning(
'Request %s had missing message mappings %s', request, result)
return [id_to_message.get(random_to_id.get(rnd)) for rnd in random_to_id]
# endregion

File diff suppressed because it is too large Load Diff

View File

@ -1,682 +0,0 @@
import abc
import asyncio
import collections
import logging
import platform
import time
import typing
from .. import version, helpers, __name__ as __base_name__
from ..crypto import rsa
from ..entitycache import EntityCache
from ..extensions import markdown
from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy
from ..sessions import Session, SQLiteSession, MemorySession
from ..statecache import StateCache
from ..tl import TLObject, functions, types
from ..tl.alltlobjects import LAYER
DEFAULT_DC_ID = 4
DEFAULT_IPV4_IP = '149.154.167.51'
DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]'
DEFAULT_PORT = 443
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
__default_log__ = logging.getLogger(__base_name__)
__default_log__.addHandler(logging.NullHandler())
# TODO How hard would it be to support both `trio` and `asyncio`?
class TelegramBaseClient(abc.ABC):
"""
This is the abstract base class for the client. It defines some
basic stuff like connecting, switching data center, etc, and
leaves the `__call__` unimplemented.
Arguments
session (`str` | `telethon.sessions.abstract.Session`, `None`):
The file name of the session file to be used if a string is
given (it may be a full path), or the Session instance to be
used otherwise. If it's ``None``, the session will not be saved,
and you should call :meth:`.log_out()` when you're done.
Note that if you pass a string it will be a file in the current
working directory, although you can also pass absolute paths.
The session file contains enough information for you to login
without re-sending the code, so if you have to enter the code
more than once, maybe you're changing the working directory,
renaming or removing the file, or using random names.
api_id (`int` | `str`):
The API ID you obtained from https://my.telegram.org.
api_hash (`str`):
The API ID you obtained from https://my.telegram.org.
connection (`telethon.network.connection.common.Connection`, optional):
The connection instance to be used when creating a new connection
to the servers. It **must** be a type.
Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`.
use_ipv6 (`bool`, optional):
Whether to connect to the servers through IPv6 or not.
By default this is ``False`` as IPv6 support is not
too widespread yet.
proxy (`tuple` | `list` | `dict`, optional):
An iterable consisting of the proxy info. If `connection` is
one of `MTProxy`, then it should contain MTProxy credentials:
``('hostname', port, 'secret')``. Otherwise, it's meant to store
function parameters for PySocks, like ``(type, 'hostname', port)``.
See https://github.com/Anorov/PySocks#usage-1 for more.
timeout (`int` | `float`, optional):
The timeout in seconds to be used when connecting.
This is **not** the timeout to be used when ``await``'ing for
invoked requests, and you should use ``asyncio.wait`` or
``asyncio.wait_for`` for that.
request_retries (`int` | `None`, optional):
How many times a request should be retried. Request are retried
when Telegram is having internal issues (due to either
``errors.ServerError`` or ``errors.RpcCallFailError``),
when there is a ``errors.FloodWaitError`` less than
`flood_sleep_threshold`, or when there's a migrate error.
May take a negative or ``None`` value for infinite retries, but
this is not recommended, since some requests can always trigger
a call fail (such as searching for messages).
connection_retries (`int` | `None`, optional):
How many times the reconnection should retry, either on the
initial connection or when Telegram disconnects us. May be
set to a negative or ``None`` value for infinite retries, but
this is not recommended, since the program can get stuck in an
infinite loop.
retry_delay (`int` | `float`, optional):
The delay in seconds to sleep between automatic reconnections.
auto_reconnect (`bool`, optional):
Whether reconnection should be retried `connection_retries`
times automatically if Telegram disconnects us or not.
sequential_updates (`bool`, optional):
By default every incoming update will create a new task, so
you can handle several updates in parallel. Some scripts need
the order in which updates are processed to be sequential, and
this setting allows them to do so.
If set to ``True``, incoming updates will be put in a queue
and processed sequentially. This means your event handlers
should *not* perform long-running operations since new
updates are put inside of an unbounded queue.
flood_sleep_threshold (`int` | `float`, optional):
The threshold below which the library should automatically
sleep on flood wait errors (inclusive). For instance, if a
``FloodWaitError`` for 17s occurs and `flood_sleep_threshold`
is 20s, the library will ``sleep`` automatically. If the error
was for 21s, it would ``raise FloodWaitError`` instead. Values
larger than a day (like ``float('inf')``) will be changed to a day.
device_model (`str`, optional):
"Device model" to be sent when creating the initial connection.
Defaults to ``platform.node()``.
system_version (`str`, optional):
"System version" to be sent when creating the initial connection.
Defaults to ``platform.system()``.
app_version (`str`, optional):
"App version" to be sent when creating the initial connection.
Defaults to `telethon.version.__version__`.
lang_code (`str`, optional):
"Language code" to be sent when creating the initial connection.
Defaults to ``'en'``.
system_lang_code (`str`, optional):
"System lang code" to be sent when creating the initial connection.
Defaults to `lang_code`.
loop (`asyncio.AbstractEventLoop`, optional):
Asyncio event loop to use. Defaults to `asyncio.get_event_loop()`
base_logger (`str` | `logging.Logger`, optional):
Base logger name or instance to use.
If a `str` is given, it'll be passed to `logging.getLogger()`. If a
`logging.Logger` is given, it'll be used directly. If something
else or nothing is given, the default logger will be used.
"""
# Current TelegramClient version
__version__ = version.__version__
# Cached server configuration (with .dc_options), can be "global"
_config = None
_cdn_config = None
# region Initialization
def __init__(
self: 'TelegramClient',
session: 'typing.Union[str, Session]',
api_id: int,
api_hash: str,
*,
connection: 'typing.Type[Connection]' = ConnectionTcpFull,
use_ipv6: bool = False,
proxy: typing.Union[tuple, dict] = None,
timeout: int = 10,
request_retries: int = 5,
connection_retries: int =5,
retry_delay: int = 1,
auto_reconnect: bool = True,
sequential_updates: bool = False,
flood_sleep_threshold: int = 60,
device_model: str = None,
system_version: str = None,
app_version: str = None,
lang_code: str = 'en',
system_lang_code: str = 'en',
loop: asyncio.AbstractEventLoop = None,
base_logger: typing.Union[str, logging.Logger] = None):
if not api_id or not api_hash:
raise ValueError(
"Your API ID or Hash cannot be empty or None. "
"Refer to telethon.rtfd.io for more information.")
self._use_ipv6 = use_ipv6
self._loop = loop or asyncio.get_event_loop()
if isinstance(base_logger, str):
base_logger = logging.getLogger(base_logger)
elif not isinstance(base_logger, logging.Logger):
base_logger = __default_log__
class _Loggers(dict):
def __missing__(self, key):
if key.startswith("telethon."):
key = key.split('.', maxsplit=1)[1]
return base_logger.getChild(key)
self._log = _Loggers()
# Determine what session object we have
if isinstance(session, str) or session is None:
try:
session = SQLiteSession(session)
except ImportError:
import warnings
warnings.warn(
'The sqlite3 module is not available under this '
'Python installation and no custom session '
'instance was given; using MemorySession.\n'
'You will need to re-login every time unless '
'you use another session storage'
)
session = MemorySession()
elif not isinstance(session, Session):
raise TypeError(
'The given session must be a str or a Session instance.'
)
# ':' in session.server_address is True if it's an IPv6 address
if (not session.server_address or
(':' in session.server_address) != use_ipv6):
session.set_dc(
DEFAULT_DC_ID,
DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP,
DEFAULT_PORT
)
self.flood_sleep_threshold = flood_sleep_threshold
# TODO Use AsyncClassWrapper(session)
# ChatGetter and SenderGetter can use the in-memory _entity_cache
# to avoid network access and the need for await in session files.
#
# The session files only wants the entities to persist
# them to disk, and to save additional useful information.
# TODO Session should probably return all cached
# info of entities, not just the input versions
self.session = session
self._entity_cache = EntityCache()
self.api_id = int(api_id)
self.api_hash = api_hash
self._request_retries = request_retries
self._connection_retries = connection_retries
self._retry_delay = retry_delay or 0
self._proxy = proxy
self._timeout = timeout
self._auto_reconnect = auto_reconnect
assert isinstance(connection, type)
self._connection = connection
init_proxy = None if not issubclass(connection, TcpMTProxy) else \
types.InputClientProxy(*connection.address_info(proxy))
# Used on connection. Capture the variables in a lambda since
# exporting clients need to create this InvokeWithLayerRequest.
system = platform.uname()
self._init_with = lambda x: functions.InvokeWithLayerRequest(
LAYER, functions.InitConnectionRequest(
api_id=self.api_id,
device_model=device_model or system.system or 'Unknown',
system_version=system_version or system.release or '1.0',
app_version=app_version or self.__version__,
lang_code=lang_code,
system_lang_code=system_lang_code,
lang_pack='', # "langPacks are for official apps only"
query=x,
proxy=init_proxy
)
)
self._sender = MTProtoSender(
self.session.auth_key, self._loop,
loggers=self._log,
retries=self._connection_retries,
delay=self._retry_delay,
auto_reconnect=self._auto_reconnect,
connect_timeout=self._timeout,
auth_key_callback=self._auth_key_callback,
update_callback=self._handle_update,
auto_reconnect_callback=self._handle_auto_reconnect
)
# Remember flood-waited requests to avoid making them again
self._flood_waited_requests = {}
# Cache ``{dc_id: (n, MTProtoSender)}`` for all borrowed senders,
# being ``n`` the amount of borrows a given sender has; once ``n``
# reaches ``0`` it should be disconnected and removed.
self._borrowed_senders = {}
self._borrow_sender_lock = asyncio.Lock(loop=self._loop)
self._updates_handle = None
self._last_request = time.time()
self._channel_pts = {}
if sequential_updates:
self._updates_queue = asyncio.Queue(loop=self._loop)
self._dispatching_updates_queue = asyncio.Event(loop=self._loop)
else:
# Use a set of pending instead of a queue so we can properly
# terminate all pending updates on disconnect.
self._updates_queue = set()
self._dispatching_updates_queue = None
self._authorized = None # None = unknown, False = no, True = yes
# Update state (for catching up after a disconnection)
# TODO Get state from channels too
self._state_cache = StateCache(
self.session.get_update_state(0), self._log)
# Some further state for subclasses
self._event_builders = []
# {chat_id: {Conversation}}
self._conversations = collections.defaultdict(set)
# Default parse mode
self._parse_mode = markdown
# Some fields to easy signing in. Let {phone: hash} be
# a dictionary because the user may change their mind.
self._phone_code_hash = {}
self._phone = None
self._tos = None
# Sometimes we need to know who we are, cache the self peer
self._self_input_peer = None
self._bot = None
# endregion
# region Properties
@property
def loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop:
"""
Property with the ``asyncio`` event loop used by this client.
Example
.. code-block:: python
# Download media in the background
task = client.loop_create_task(message.download_media())
# Do some work
...
# Join the task (wait for it to complete)
await task
"""
return self._loop
@property
def disconnected(self: 'TelegramClient') -> asyncio.Future:
"""
Property with a ``Future`` that resolves upon disconnection.
Example
.. code-block:: python
# Wait for a disconnection to occur
try:
await client.disconnected
except OSError:
print('Error on disconnect')
"""
return self._sender.disconnected
# endregion
# region Connecting
async def connect(self: 'TelegramClient') -> None:
"""
Connects to Telegram.
.. note::
Connect means connect and nothing else, and only one low-level
request is made to notify Telegram about which layer we will be
using.
Before Telegram sends you updates, you need to make a high-level
request, like `client.get_me() <telethon.client.users.UserMethods.get_me>`,
as described in https://core.telegram.org/api/updates.
Example
.. code-block:: python
try:
client.connect()
except OSError:
print('Failed to connect')
"""
await self._sender.connect(self._connection(
self.session.server_address,
self.session.port,
self.session.dc_id,
loop=self._loop,
loggers=self._log,
proxy=self._proxy
))
self.session.auth_key = self._sender.auth_key
self.session.save()
await self._sender.send(self._init_with(
functions.help.GetConfigRequest()))
self._updates_handle = self._loop.create_task(self._update_loop())
def is_connected(self: 'TelegramClient') -> bool:
"""
Returns ``True`` if the user has connected.
This method is **not** asynchronous (don't use ``await`` on it).
Example
.. code-block:: python
while client.is_connected():
await asyncio.sleep(1)
"""
sender = getattr(self, '_sender', None)
return sender and sender.is_connected()
def disconnect(self: 'TelegramClient'):
"""
Disconnects from Telegram.
If the event loop is already running, this method returns a
coroutine that you should await on your own code; otherwise
the loop is ran until said coroutine completes.
Example
.. code-block:: python
# You don't need to use this if you used "with client"
client.disconnect()
"""
if self._loop.is_running():
return self._disconnect_coro()
else:
try:
self._loop.run_until_complete(self._disconnect_coro())
except RuntimeError:
# Python 3.5.x complains when called from
# `__aexit__` and there were pending updates with:
# "Event loop stopped before Future completed."
#
# However, it doesn't really make a lot of sense.
pass
async def _disconnect_coro(self: 'TelegramClient'):
await self._disconnect()
# trio's nurseries would handle this for us, but this is asyncio.
# All tasks spawned in the background should properly be terminated.
if self._dispatching_updates_queue is None and self._updates_queue:
for task in self._updates_queue:
task.cancel()
await asyncio.wait(self._updates_queue, loop=self._loop)
self._updates_queue.clear()
pts, date = self._state_cache[None]
if pts and date:
self.session.set_update_state(0, types.updates.State(
pts=pts,
qts=0,
date=date,
seq=0,
unread_count=0
))
self.session.close()
async def _disconnect(self: 'TelegramClient'):
"""
Disconnect only, without closing the session. Used in reconnections
to different data centers, where we don't want to close the session
file; user disconnects however should close it since it means that
their job with the client is complete and we should clean it up all.
"""
await self._sender.disconnect()
await helpers._cancel(self._log[__name__],
updates_handle=self._updates_handle)
async def _switch_dc(self: 'TelegramClient', new_dc):
"""
Permanently switches the current connection to the new data center.
"""
self._log[__name__].info('Reconnecting to new data center %s', new_dc)
dc = await self._get_dc(new_dc)
self.session.set_dc(dc.id, dc.ip_address, dc.port)
# auth_key's are associated with a server, which has now changed
# so it's not valid anymore. Set to None to force recreating it.
self._sender.auth_key.key = None
self.session.auth_key = None
self.session.save()
await self._disconnect()
return await self.connect()
def _auth_key_callback(self: 'TelegramClient', auth_key):
"""
Callback from the sender whenever it needed to generate a
new authorization key. This means we are not authorized.
"""
self.session.auth_key = auth_key
self.session.save()
# endregion
# region Working with different connections/Data Centers
async def _get_dc(self: 'TelegramClient', dc_id, cdn=False):
"""Gets the Data Center (DC) associated to 'dc_id'"""
cls = self.__class__
if not cls._config:
cls._config = await self(functions.help.GetConfigRequest())
if cdn and not self._cdn_config:
cls._cdn_config = await self(functions.help.GetCdnConfigRequest())
for pk in cls._cdn_config.public_keys:
rsa.add_key(pk.public_key)
return next(
dc for dc in cls._config.dc_options
if dc.id == dc_id
and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn
)
async def _create_exported_sender(self: 'TelegramClient', dc_id):
"""
Creates a new exported `MTProtoSender` for the given `dc_id` and
returns it. This method should be used by `_borrow_exported_sender`.
"""
# Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt
# for clearly showing how to export the authorization
dc = await self._get_dc(dc_id)
# Can't reuse self._sender._connection as it has its own seqno.
#
# If one were to do that, Telegram would reset the connection
# with no further clues.
sender = MTProtoSender(None, self._loop, loggers=self._log)
await sender.connect(self._connection(
dc.ip_address,
dc.port,
dc.id,
loop=self._loop,
loggers=self._log,
proxy=self._proxy
))
self._log[__name__].info('Exporting authorization for data center %s',
dc)
auth = await self(functions.auth.ExportAuthorizationRequest(dc_id))
req = self._init_with(functions.auth.ImportAuthorizationRequest(
id=auth.id, bytes=auth.bytes
))
await sender.send(req)
return sender
async def _borrow_exported_sender(self: 'TelegramClient', dc_id):
"""
Borrows a connected `MTProtoSender` for the given `dc_id`.
If it's not cached, creates a new one if it doesn't exist yet,
and imports a freshly exported authorization key for it to be usable.
Once its job is over it should be `_return_exported_sender`.
"""
async with self._borrow_sender_lock:
n, sender = self._borrowed_senders.get(dc_id, (0, None))
if not sender:
sender = await self._create_exported_sender(dc_id)
sender.dc_id = dc_id
elif not n:
dc = await self._get_dc(dc_id)
await sender.connect(self._connection(
dc.ip_address,
dc.port,
dc.id,
loop=self._loop,
loggers=self._log,
proxy=self._proxy
))
self._borrowed_senders[dc_id] = (n + 1, sender)
return sender
async def _return_exported_sender(self: 'TelegramClient', sender):
"""
Returns a borrowed exported sender. If all borrows have
been returned, the sender is cleanly disconnected.
"""
async with self._borrow_sender_lock:
dc_id = sender.dc_id
n, _ = self._borrowed_senders[dc_id]
n -= 1
self._borrowed_senders[dc_id] = (n, sender)
if not n:
self._log[__name__].info(
'Disconnecting borrowed sender for DC %d', dc_id)
await sender.disconnect()
async def _get_cdn_client(self: 'TelegramClient', cdn_redirect):
"""Similar to ._borrow_exported_client, but for CDNs"""
# TODO Implement
raise NotImplementedError
session = self._exported_sessions.get(cdn_redirect.dc_id)
if not session:
dc = await self._get_dc(cdn_redirect.dc_id, cdn=True)
session = self.session.clone()
await session.set_dc(dc.id, dc.ip_address, dc.port)
self._exported_sessions[cdn_redirect.dc_id] = session
self._log[__name__].info('Creating new CDN client')
client = TelegramBareClient(
session, self.api_id, self.api_hash,
proxy=self._sender.connection.conn.proxy,
timeout=self._sender.connection.get_timeout()
)
# This will make use of the new RSA keys for this specific CDN.
#
# We won't be calling GetConfigRequest because it's only called
# when needed by ._get_dc, and also it's static so it's likely
# set already. Avoid invoking non-CDN methods by not syncing updates.
client.connect(_sync_updates=False)
return client
# endregion
# region Invoking Telegram requests
@abc.abstractmethod
def __call__(self: 'TelegramClient', request, ordered=False):
"""
Invokes (sends) one or more MTProtoRequests and returns (receives)
their result.
Args:
request (`TLObject` | `list`):
The request or requests to be invoked.
ordered (`bool`, optional):
Whether the requests (if more than one was given) should be
executed sequentially on the server. They run in arbitrary
order by default.
Returns:
The result of the request (often a `TLObject`) or a list of
results if more than one request was given.
"""
raise NotImplementedError
@abc.abstractmethod
def _handle_update(self: 'TelegramClient', update):
raise NotImplementedError
@abc.abstractmethod
def _update_loop(self: 'TelegramClient'):
raise NotImplementedError
@abc.abstractmethod
async def _handle_auto_reconnect(self: 'TelegramClient'):
raise NotImplementedError
# endregion

View File

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

View File

@ -1,545 +0,0 @@
import asyncio
import itertools
import random
import time
import typing
from .. import events, utils, errors
from ..events.common import EventBuilder, EventCommon
from ..tl import types, functions
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
class UpdateMethods:
# region Public methods
async def _run_until_disconnected(self: 'TelegramClient'):
try:
# Make a high-level request to notify that we want updates
await self(functions.updates.GetStateRequest())
return await self.disconnected
except KeyboardInterrupt:
pass
finally:
await self.disconnect()
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.
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.
client.run_until_disconnected()
"""
if self.loop.is_running():
return self._run_until_disconnected()
try:
return self.loop.run_until_complete(self._run_until_disconnected())
except KeyboardInterrupt:
pass
finally:
# No loop.run_until_complete; it's already syncified
self.disconnect()
def on(self: 'TelegramClient', event: EventBuilder):
"""
Decorator used to `add_event_handler` more conveniently.
Arguments
event (`_EventBuilder` | `type`):
The event builder class or instance to be used,
for instance ``events.NewMessage``.
Example
.. code-block:: python
from telethon import TelegramClient, events
client = TelegramClient(...)
# Here we use client.on
@client.on(events.NewMessage)
async def handler(event):
...
"""
def decorator(f):
self.add_event_handler(f, event)
return f
return decorator
def add_event_handler(
self: 'TelegramClient',
callback: callable,
event: EventBuilder = None):
"""
Registers a new event handler callback.
The callback will be called when the specified event occurs.
Arguments
callback (`callable`):
The callable function accepting one parameter to be used.
Note that if you have used `telethon.events.register` in
the callback, ``event`` will be ignored, and instead the
events you previously registered will be used.
event (`_EventBuilder` | `type`, optional):
The event builder class or instance to be used,
for instance ``events.NewMessage``.
If left unspecified, `telethon.events.raw.Raw` (the
:tl:`Update` objects with no further processing) will
be passed instead.
Example
.. code-block:: python
from telethon import TelegramClient, events
client = TelegramClient(...)
async def handler(event):
...
client.add_event_handler(handler, events.NewMessage)
"""
builders = events._get_handlers(callback)
if builders is not None:
for event in builders:
self._event_builders.append((event, callback))
return
if isinstance(event, type):
event = event()
elif not event:
event = events.Raw()
self._event_builders.append((event, callback))
def remove_event_handler(
self: 'TelegramClient',
callback: callable,
event: EventBuilder = None) -> int:
"""
Inverse operation of `add_event_handler()`.
If no event is given, all events for this callback are removed.
Returns how many callbacks were removed.
Example
.. code-block:: python
@client.on(events.Raw)
@client.on(events.NewMessage)
async def handler(event):
...
# Removes only the "Raw" handling
# "handler" will still receive "events.NewMessage"
client.remove_event_handler(handler, events.Raw)
# "handler" will stop receiving anything
client.remove_event_handler(handler)
"""
found = 0
if event and not isinstance(event, type):
event = type(event)
i = len(self._event_builders)
while i:
i -= 1
ev, cb = self._event_builders[i]
if cb == callback and (not event or isinstance(ev, event)):
del self._event_builders[i]
found += 1
return found
def list_event_handlers(self: 'TelegramClient')\
-> 'typing.Sequence[typing.Tuple[callable, EventBuilder]]':
"""
Lists all registered event handlers.
Returns
A list of pairs consisting of ``(callback, event)``.
Example
.. code-block:: python
@client.on(events.NewMessage(pattern='hello'))
async def on_greeting(event):
'''Greets someone'''
await event.reply('Hi')
for callback, event in client.list_event_handlers():
print(id(callback), type(event))
"""
return [(callback, event) for event, callback in self._event_builders]
async def catch_up(self: 'TelegramClient'):
"""
"Catches up" on the missed updates while the client was offline.
You should call this method after registering the event handlers
so that the updates it loads can by processed by your script.
This can also be used to forcibly fetch new updates if there are any.
Example
.. code-block:: python
client.catch_up()
"""
pts, date = self._state_cache[None]
if not pts:
return
self.session.catching_up = True
try:
while True:
d = await self(functions.updates.GetDifferenceRequest(
pts, date, 0
))
if isinstance(d, (types.updates.DifferenceSlice,
types.updates.Difference)):
if isinstance(d, types.updates.Difference):
state = d.state
else:
state = d.intermediate_state
pts, date = state.pts, state.date
self._handle_update(types.Updates(
users=d.users,
chats=d.chats,
date=state.date,
seq=state.seq,
updates=d.other_updates + [
types.UpdateNewMessage(m, 0, 0)
for m in d.new_messages
]
))
# TODO Implement upper limit (max_pts)
# We don't want to fetch updates we already know about.
#
# We may still get duplicates because the Difference
# contains a lot of updates and presumably only has
# the state for the last one, but at least we don't
# unnecessarily fetch too many.
#
# updates.getDifference's pts_total_limit seems to mean
# "how many pts is the request allowed to return", and
# if there is more than that, it returns "too long" (so
# there would be duplicate updates since we know about
# some). This can be used to detect collisions (i.e.
# it would return an update we have already seen).
else:
if isinstance(d, types.updates.DifferenceEmpty):
date = d.date
elif isinstance(d, types.updates.DifferenceTooLong):
pts = d.pts
break
except (ConnectionError, asyncio.CancelledError):
pass
finally:
# TODO Save new pts to session
self._state_cache._pts_date = (pts, date)
self.session.catching_up = False
# endregion
# region Private methods
# It is important to not make _handle_update async because we rely on
# the order that the updates arrive in to update the pts and date to
# be always-increasing. There is also no need to make this async.
def _handle_update(self: 'TelegramClient', update):
self.session.process_entities(update)
self._entity_cache.add(update)
if isinstance(update, (types.Updates, types.UpdatesCombined)):
entities = {utils.get_peer_id(x): x for x in
itertools.chain(update.users, update.chats)}
for u in update.updates:
self._process_update(u, entities)
elif isinstance(update, types.UpdateShort):
self._process_update(update.update)
else:
self._process_update(update)
self._state_cache.update(update)
def _process_update(self: 'TelegramClient', update, entities=None):
update._entities = entities or {}
# This part is somewhat hot so we don't bother patching
# update with channel ID/its state. Instead we just pass
# arguments which is faster.
channel_id = self._state_cache.get_channel_id(update)
args = (update, channel_id, self._state_cache[channel_id])
if self._dispatching_updates_queue is None:
task = self._loop.create_task(self._dispatch_update(*args))
self._updates_queue.add(task)
task.add_done_callback(lambda _: self._updates_queue.discard(task))
else:
self._updates_queue.put_nowait(args)
if not self._dispatching_updates_queue.is_set():
self._dispatching_updates_queue.set()
self._loop.create_task(self._dispatch_queue_updates())
self._state_cache.update(update)
async def _update_loop(self: 'TelegramClient'):
# Pings' ID don't really need to be secure, just "random"
rnd = lambda: random.randrange(-2**63, 2**63)
while self.is_connected():
try:
await asyncio.wait_for(
self.disconnected, timeout=60, loop=self._loop
)
continue # We actually just want to act upon timeout
except asyncio.TimeoutError:
pass
except asyncio.CancelledError:
return
except Exception:
continue # Any disconnected exception should be ignored
# We also don't really care about their result.
# Just send them periodically.
try:
self._sender.send(functions.PingRequest(rnd()))
except (ConnectionError, asyncio.CancelledError):
return
# Entities and cached files are not saved when they are
# inserted because this is a rather expensive operation
# (default's sqlite3 takes ~0.1s to commit changes). Do
# it every minute instead. No-op if there's nothing new.
self.session.save()
# We need to send some content-related request at least hourly
# for Telegram to keep delivering updates, otherwise they will
# just stop even if we're connected. Do so every 30 minutes.
#
# TODO Call getDifference instead since it's more relevant
if time.time() - self._last_request > 30 * 60:
if not await self.is_user_authorized():
# What can be the user doing for so
# long without being logged in...?
continue
try:
await self(functions.updates.GetStateRequest())
except (ConnectionError, asyncio.CancelledError):
return
async def _dispatch_queue_updates(self: 'TelegramClient'):
while not self._updates_queue.empty():
await self._dispatch_update(*self._updates_queue.get_nowait())
self._dispatching_updates_queue.clear()
async def _dispatch_update(self: 'TelegramClient', update, channel_id, pts_date):
if not self._entity_cache.ensure_cached(update):
# We could add a lock to not fetch the same pts twice if we are
# already fetching it. However this does not happen in practice,
# which makes sense, because different updates have different pts.
if self._state_cache.update(update, check_only=True):
# If the update doesn't have pts, fetching won't do anything.
# For example, UpdateUserStatus or UpdateChatUserTyping.
await self._get_difference(update, channel_id, pts_date)
built = EventBuilderDict(self, update)
for conv_set in self._conversations.values():
for conv in conv_set:
ev = built[events.NewMessage]
if ev:
conv._on_new_message(ev)
ev = built[events.MessageEdited]
if ev:
conv._on_edit(ev)
ev = built[events.MessageRead]
if ev:
conv._on_read(ev)
if conv._custom:
await conv._check_custom(built)
for builder, callback in self._event_builders:
event = built[type(builder)]
if not event:
continue
if not builder.resolved:
await builder.resolve(self)
if not builder.filter(event):
continue
try:
await callback(event)
except errors.AlreadyInConversationError:
name = getattr(callback, '__name__', repr(callback))
self._log[__name__].debug(
'Event handler "%s" already has an open conversation, '
'ignoring new one', name)
except events.StopPropagation:
name = getattr(callback, '__name__', repr(callback))
self._log[__name__].debug(
'Event handler "%s" stopped chain of propagation '
'for event %s.', name, type(event).__name__
)
break
except Exception as e:
if not isinstance(e, asyncio.CancelledError) or self.is_connected():
name = getattr(callback, '__name__', repr(callback))
self._log[__name__].exception('Unhandled exception on %s',
name)
async def _get_difference(self: 'TelegramClient', update, channel_id, pts_date):
"""
Get the difference for this `channel_id` if any, then load entities.
Calls :tl:`updates.getDifference`, which fills the entities cache
(always done by `__call__`) and lets us know about the full entities.
"""
# Fetch since the last known pts/date before this update arrived,
# in order to fetch this update at full, including its entities.
self._log[__name__].debug('Getting difference for entities '
'for %r', update.__class__)
if channel_id:
try:
where = await self.get_input_entity(channel_id)
except ValueError:
# There's a high chance that this fails, since
# we are getting the difference to fetch entities.
return
if not pts_date:
# First-time, can't get difference. Get pts instead.
result = await self(functions.messages.GetPeerDialogsRequest([
utils.get_input_dialog(where)
]))
self._state_cache[channel_id] = result.dialogs[0].pts
return
result = await self(functions.updates.GetChannelDifferenceRequest(
channel=where,
filter=types.ChannelMessagesFilterEmpty(),
pts=pts_date, # just pts
limit=100,
force=True
))
else:
if not pts_date[0]:
# First-time, can't get difference. Get pts instead.
result = await self(functions.updates.GetStateRequest())
self._state_cache[None] = result.pts, result.date
return
result = await self(functions.updates.GetDifferenceRequest(
pts=pts_date[0],
date=pts_date[1],
qts=0
))
if isinstance(result, (types.updates.Difference,
types.updates.DifferenceSlice,
types.updates.ChannelDifference,
types.updates.ChannelDifferenceTooLong)):
update._entities.update({
utils.get_peer_id(x): x for x in
itertools.chain(result.users, result.chats)
})
async def _handle_auto_reconnect(self: 'TelegramClient'):
# TODO Catch-up
return
try:
self._log[__name__].info(
'Asking for the current state after reconnect...')
# TODO consider:
# If there aren't many updates while the client is disconnected
# (I tried with up to 20), Telegram seems to send them without
# asking for them (via updates.getDifference).
#
# On disconnection, the library should probably set a "need
# difference" or "catching up" flag so that any new updates are
# ignored, and then the library should call updates.getDifference
# itself to fetch them.
#
# In any case (either there are too many updates and Telegram
# didn't send them, or there isn't a lot and Telegram sent them
# but we dropped them), we fetch the new difference to get all
# missed updates. I feel like this would be the best solution.
# If a disconnection occurs, the old known state will be
# the latest one we were aware of, so we can catch up since
# the most recent state we were aware of.
await self.catch_up()
self._log[__name__].info('Successfully fetched missed updates')
except errors.RPCError as e:
self._log[__name__].warning('Failed to get missed updates after '
'reconnect: %r', e)
except Exception:
self._log[__name__].exception('Unhandled exception while getting '
'update difference after reconnect')
# endregion
class EventBuilderDict:
"""
Helper "dictionary" to return events from types and cache them.
"""
def __init__(self, client: 'TelegramClient', update):
self.client = client
self.update = update
def __getitem__(self, builder):
try:
return self.__dict__[builder]
except KeyError:
event = self.__dict__[builder] = builder.build(self.update)
if isinstance(event, EventCommon):
event.original_update = self.update
event._set_client(self.client)
elif event:
event._client = self.client
return event

View File

@ -1,697 +0,0 @@
import hashlib
import io
import itertools
import os
import pathlib
import re
import typing
from io import BytesIO
from .. import utils, helpers, hints
from ..tl import types, functions, custom
try:
import PIL
import PIL.Image
except ImportError:
PIL = None
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
class _CacheType:
"""Like functools.partial but pretends to be the wrapped class."""
def __init__(self, cls):
self._cls = cls
def __call__(self, *args, **kwargs):
return self._cls(*args, file_reference=b'', **kwargs)
def __eq__(self, other):
return self._cls == other
def _resize_photo_if_needed(
file, is_image, width=1280, height=1280, background=(255, 255, 255)):
# https://github.com/telegramdesktop/tdesktop/blob/12905f0dcb9d513378e7db11989455a1b764ef75/Telegram/SourceFiles/boxes/photo_crop_box.cpp#L254
if (not is_image
or PIL is None
or (isinstance(file, io.IOBase) and not file.seekable())):
return file
if isinstance(file, bytes):
file = io.BytesIO(file)
before = file.tell() if isinstance(file, io.IOBase) else None
try:
# Don't use a `with` block for `image`, or `file` would be closed.
# See https://github.com/LonamiWebs/Telethon/issues/1121 for more.
image = PIL.Image.open(file)
if image.width <= width and image.height <= height:
return file
image.thumbnail((width, height), PIL.Image.ANTIALIAS)
alpha_index = image.mode.find('A')
if alpha_index == -1:
# If the image mode doesn't have alpha
# channel then don't bother masking it away.
result = image
else:
# We could save the resized image with the original format, but
# JPEG often compresses better -> smaller size -> faster upload
# We need to mask away the alpha channel ([3]), since otherwise
# IOError is raised when trying to save alpha channels in JPEG.
result = PIL.Image.new('RGB', image.size, background)
result.paste(image, mask=image.split()[alpha_index])
buffer = io.BytesIO()
result.save(buffer, 'JPEG')
buffer.seek(0)
return buffer
except IOError:
return file
finally:
if before is not None:
file.seek(before, io.SEEK_SET)
class UploadMethods:
# region Public methods
async def send_file(
self: 'TelegramClient',
entity: 'hints.EntityLike',
file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]',
*,
caption: typing.Union[str, typing.Sequence[str]] = None,
force_document: bool = False,
progress_callback: 'hints.ProgressCallback' = None,
reply_to: 'hints.MessageIDLike' = None,
attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,
thumb: 'hints.FileLike' = None,
allow_cache: bool = True,
parse_mode: str = (),
voice_note: bool = False,
video_note: bool = False,
buttons: 'hints.MarkupLike' = None,
silent: bool = None,
supports_streaming: bool = False,
**kwargs) -> 'types.Message':
"""
Sends message with the given file to the specified entity.
.. note::
If the ``hachoir3`` package (``hachoir`` module) is installed,
it will be used to determine metadata from audio and video files.
If the ``pillow`` package is installed and you are sending a photo,
it will be resized to fit within the maximum dimensions allowed
by Telegram to avoid ``errors.PhotoInvalidDimensionsError``. This
cannot be done if you are sending :tl:`InputFile`, however.
Arguments
entity (`entity`):
Who will receive the file.
file (`str` | `bytes` | `file` | `media`):
The file to send, which can be one of:
* A local file path to an in-disk file. The file name
will be the path's base name.
* A `bytes` byte array with the file's data to send
(for example, by using ``text.encode('utf-8')``).
A default file name will be used.
* A bytes `io.IOBase` stream over the file to send
(for example, by using ``open(file, 'rb')``).
Its ``.name`` property will be used for the file name,
or a default if it doesn't have one.
* An external URL to a file over the internet. This will
send the file as "external" media, and Telegram is the
one that will fetch the media and send it.
* A Bot API-like ``file_id``. You can convert previously
sent media to file IDs for later reusing with
`telethon.utils.pack_bot_file_id`.
* A handle to an existing file (for example, if you sent a
message with media before, you can use its ``message.media``
as a file here).
* A handle to an uploaded file (from `upload_file`).
To send an album, you should provide a list in this parameter.
If a list or similar is provided, the files in it will be
sent as an album in the order in which they appear, sliced
in chunks of 10 if more than 10 are given.
caption (`str`, optional):
Optional caption for the sent media message. When sending an
album, the caption may be a list of strings, which will be
assigned to the files pairwise.
force_document (`bool`, optional):
If left to ``False`` and the file is a path that ends with
the extension of an image file or a video file, it will be
sent as such. Otherwise always as a document.
progress_callback (`callable`, optional):
A callback function accepting two parameters:
``(sent bytes, total)``.
reply_to (`int` | `Message <telethon.tl.custom.message.Message>`):
Same as `reply_to` from `send_message`.
attributes (`list`, optional):
Optional attributes that override the inferred ones, like
:tl:`DocumentAttributeFilename` and so on.
thumb (`str` | `bytes` | `file`, optional):
Optional JPEG thumbnail (for documents). **Telegram will
ignore this parameter** unless you pass a ``.jpg`` file!
The file must also be small in dimensions and in-disk size.
Successful thumbnails were files below 20kb and 200x200px.
Width/height and dimensions/size ratios may be important.
allow_cache (`bool`, optional):
Whether to allow using the cached version stored in the
database or not. Defaults to ``True`` to avoid re-uploads.
Must be ``False`` if you wish to use different attributes
or thumb than those that were used when the file was cached.
parse_mode (`object`, optional):
See the `TelegramClient.parse_mode
<telethon.client.messageparse.MessageParseMethods.parse_mode>`
property for allowed values. Markdown parsing will be used by
default.
voice_note (`bool`, optional):
If ``True`` the audio will be sent as a voice note.
Set `allow_cache` to ``False`` if you sent the same file
without this setting before for it to work.
video_note (`bool`, optional):
If ``True`` the video will be sent as a video note,
also known as a round video message.
Set `allow_cache` to ``False`` if you sent the same file
without this setting before for it to work.
buttons (`list`, `custom.Button <telethon.tl.custom.button.Button>`, :tl:`KeyboardButton`):
The matrix (list of lists), row list or button to be shown
after sending the message. This parameter will only work if
you have signed in as a bot. You can also pass your own
:tl:`ReplyMarkup` here.
silent (`bool`, optional):
Whether the message should notify people in a broadcast
channel or not. Defaults to ``False``, which means it will
notify them. Set it to ``True`` to alter this behaviour.
supports_streaming (`bool`, optional):
Whether the sent video supports streaming or not. Note that
Telegram only recognizes as streamable some formats like MP4,
and others like AVI or MKV will not work. You should convert
these to MP4 before sending if you want them to be streamable.
Unsupported formats will result in ``VideoContentTypeError``.
Returns
The `Message <telethon.tl.custom.message.Message>` (or messages)
containing the sent file, or messages if a list of them was passed.
Example
.. code-block:: python
# Normal files like photos
client.send_file(chat, '/my/photos/me.jpg', caption="It's me!")
# or
client.send_message(chat, "It's me!", file='/my/photos/me.jpg')
# Voice notes or round videos
client.send_file(chat, '/my/songs/song.mp3', voice_note=True)
client.send_file(chat, '/my/videos/video.mp4', video_note=True)
# Custom thumbnails
client.send_file(chat, '/my/documents/doc.txt', thumb='photo.jpg')
# Only documents
client.send_file(chat, '/my/photos/photo.png', force_document=True)
# Albums
client.send_file(chat, [
'/my/photos/holiday1.jpg',
'/my/photos/holiday2.jpg',
'/my/drawings/portrait.png'
])
"""
# i.e. ``None`` was used
if not file:
raise TypeError('Cannot use {!r} as file'.format(file))
if not caption:
caption = ''
# First check if the user passed an iterable, in which case
# we may want to send as an album if all are photo files.
if utils.is_list_like(file):
image_captions = []
document_captions = []
if utils.is_list_like(caption):
captions = caption
else:
captions = [caption]
# TODO Fix progress_callback
images = []
if force_document:
documents = file
else:
documents = []
for doc, cap in itertools.zip_longest(file, captions):
if utils.is_image(doc):
images.append(doc)
image_captions.append(cap)
else:
documents.append(doc)
document_captions.append(cap)
result = []
while images:
result += await self._send_album(
entity, images[:10], caption=image_captions[:10],
progress_callback=progress_callback, reply_to=reply_to,
parse_mode=parse_mode, silent=silent
)
images = images[10:]
image_captions = image_captions[10:]
for doc, cap in zip(documents, captions):
result.append(await self.send_file(
entity, doc, allow_cache=allow_cache,
caption=cap, force_document=force_document,
progress_callback=progress_callback, reply_to=reply_to,
attributes=attributes, thumb=thumb, voice_note=voice_note,
video_note=video_note, buttons=buttons, silent=silent,
supports_streaming=supports_streaming,
**kwargs
))
return result
entity = await self.get_input_entity(entity)
reply_to = utils.get_message_id(reply_to)
# Not document since it's subject to change.
# Needed when a Message is passed to send_message and it has media.
if 'entities' in kwargs:
msg_entities = kwargs['entities']
else:
caption, msg_entities =\
await self._parse_message_text(caption, parse_mode)
file_handle, media, image = await self._file_to_media(
file, force_document=force_document,
progress_callback=progress_callback,
attributes=attributes, allow_cache=allow_cache, thumb=thumb,
voice_note=voice_note, video_note=video_note,
supports_streaming=supports_streaming
)
# e.g. invalid cast from :tl:`MessageMediaWebPage`
if not media:
raise TypeError('Cannot use {!r} as file'.format(file))
markup = self.build_reply_markup(buttons)
request = functions.messages.SendMediaRequest(
entity, media, reply_to_msg_id=reply_to, message=caption,
entities=msg_entities, reply_markup=markup, silent=silent
)
msg = self._get_response_message(request, await self(request), entity)
await self._cache_media(msg, file, file_handle, image=image)
return msg
async def _send_album(self: 'TelegramClient', entity, files, caption='',
progress_callback=None, reply_to=None,
parse_mode=(), silent=None):
"""Specialized version of .send_file for albums"""
# We don't care if the user wants to avoid cache, we will use it
# anyway. Why? The cached version will be exactly the same thing
# we need to produce right now to send albums (uploadMedia), and
# cache only makes a difference for documents where the user may
# want the attributes used on them to change.
#
# In theory documents can be sent inside the albums but they appear
# as different messages (not inside the album), and the logic to set
# the attributes/avoid cache is already written in .send_file().
entity = await self.get_input_entity(entity)
if not utils.is_list_like(caption):
caption = (caption,)
captions = []
for c in reversed(caption): # Pop from the end (so reverse)
captions.append(await self._parse_message_text(c or '', parse_mode))
reply_to = utils.get_message_id(reply_to)
# Need to upload the media first, but only if they're not cached yet
media = []
for file in files:
# Albums want :tl:`InputMedia` which, in theory, includes
# :tl:`InputMediaUploadedPhoto`. However using that will
# make it `raise MediaInvalidError`, so we need to upload
# it as media and then convert that to :tl:`InputMediaPhoto`.
fh, fm, _ = await self._file_to_media(file)
if isinstance(fm, types.InputMediaUploadedPhoto):
r = await self(functions.messages.UploadMediaRequest(
entity, media=fm
))
self.session.cache_file(
fh.md5, fh.size, utils.get_input_photo(r.photo))
fm = utils.get_input_media(r.photo)
if captions:
caption, msg_entities = captions.pop()
else:
caption, msg_entities = '', None
media.append(types.InputSingleMedia(
fm,
message=caption,
entities=msg_entities
))
# Now we can construct the multi-media request
result = await self(functions.messages.SendMultiMediaRequest(
entity, reply_to_msg_id=reply_to, multi_media=media, silent=silent
))
# We never sent a `random_id` for the messages that resulted from
# the request so we can't pair them up with the `Updates` that we
# get from Telegram. However, the sent messages have a photo and
# the photo IDs match with those we did send.
#
# Updates -> {_: message}
messages = self._get_response_message(None, result, entity)
# {_: message} -> {photo ID: message}
messages = {m.photo.id: m for m in messages.values()}
# Sent photo IDs -> messages
return [messages[m.media.id.id] for m in media]
async def upload_file(
self: 'TelegramClient',
file: 'hints.FileLike',
*,
part_size_kb: float = None,
file_name: str = None,
use_cache: type = None,
progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile':
"""
Uploads a file to Telegram's servers, without sending it.
This method returns a handle (an instance of :tl:`InputFile` or
:tl:`InputFileBig`, as required) which can be later used before
it expires (they are usable during less than a day).
Uploading a file will simply return a "handle" to the file stored
remotely in the Telegram servers, which can be later used on. This
will **not** upload the file to your own chat or any chat at all.
Arguments
file (`str` | `bytes` | `file`):
The path of the file, byte array, or stream that will be sent.
Note that if a byte array or a stream is given, a filename
or its type won't be inferred, and it will be sent as an
"unnamed application/octet-stream".
part_size_kb (`int`, optional):
Chunk size when uploading files. The larger, the less
requests will be made (up to 512KB maximum).
file_name (`str`, optional):
The file name which will be used on the resulting InputFile.
If not specified, the name will be taken from the ``file``
and if this is not a ``str``, it will be ``"unnamed"``.
use_cache (`type`, optional):
The type of cache to use (currently either :tl:`InputDocument`
or :tl:`InputPhoto`). If present and the file is small enough
to need the MD5, it will be checked against the database,
and if a match is found, the upload won't be made. Instead,
an instance of type ``use_cache`` will be returned.
progress_callback (`callable`, optional):
A callback function accepting two parameters:
``(sent bytes, total)``.
Returns
:tl:`InputFileBig` if the file size is larger than 10MB,
`InputSizedFile <telethon.tl.custom.inputsizedfile.InputSizedFile>`
(subclass of :tl:`InputFile`) otherwise.
Example
.. code-block:: python
# Photos as photo and document
file = client.upload_file('photo.jpg')
client.send_file(chat, file) # sends as photo
client.send_file(chat, file, force_document=True) # sends as document
file.name = 'not a photo.jpg'
client.send_file(chat, file, force_document=True) # document, new name
# As song or as voice note
file = client.upload_file('song.ogg')
client.send_file(chat, file) # sends as song
client.send_file(chat, file, voice_note=True) # sends as voice note
"""
if isinstance(file, (types.InputFile, types.InputFileBig)):
return file # Already uploaded
if not file_name and getattr(file, 'name', None):
file_name = file.name
if isinstance(file, str):
file_size = os.path.getsize(file)
elif isinstance(file, bytes):
file_size = len(file)
else:
if isinstance(file, io.IOBase) and file.seekable():
pos = file.tell()
else:
pos = None
# TODO Don't load the entire file in memory always
data = file.read()
if pos is not None:
file.seek(pos)
file = data
file_size = len(file)
# File will now either be a string or bytes
if not part_size_kb:
part_size_kb = utils.get_appropriated_part_size(file_size)
if part_size_kb > 512:
raise ValueError('The part size must be less or equal to 512KB')
part_size = int(part_size_kb * 1024)
if part_size % 1024 != 0:
raise ValueError(
'The part size must be evenly divisible by 1024')
# Set a default file name if None was specified
file_id = helpers.generate_random_long()
if not file_name:
if isinstance(file, str):
file_name = os.path.basename(file)
else:
file_name = str(file_id)
# If the file name lacks extension, add it if possible.
# Else Telegram complains with `PHOTO_EXT_INVALID_ERROR`
# even if the uploaded image is indeed a photo.
if not os.path.splitext(file_name)[-1]:
file_name += utils._get_extension(file)
# Determine whether the file is too big (over 10MB) or not
# Telegram does make a distinction between smaller or larger files
is_large = file_size > 10 * 1024 * 1024
hash_md5 = hashlib.md5()
if not is_large:
# Calculate the MD5 hash before anything else.
# As this needs to be done always for small files,
# might as well do it before anything else and
# check the cache.
if isinstance(file, str):
with open(file, 'rb') as stream:
file = stream.read()
hash_md5.update(file)
if use_cache:
cached = self.session.get_file(
hash_md5.digest(), file_size, cls=_CacheType(use_cache)
)
if cached:
return cached
part_count = (file_size + part_size - 1) // part_size
self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d',
file_size, part_count, part_size)
with open(file, 'rb') if isinstance(file, str) else BytesIO(file)\
as stream:
for part_index in range(part_count):
# Read the file by in chunks of size part_size
part = stream.read(part_size)
# The SavePartRequest is different depending on whether
# the file is too large or not (over or less than 10MB)
if is_large:
request = functions.upload.SaveBigFilePartRequest(
file_id, part_index, part_count, part)
else:
request = functions.upload.SaveFilePartRequest(
file_id, part_index, part)
result = await self(request)
if result:
self._log[__name__].debug('Uploaded %d/%d',
part_index + 1, part_count)
if progress_callback:
progress_callback(stream.tell(), file_size)
else:
raise RuntimeError(
'Failed to upload file part {}.'.format(part_index))
if is_large:
return types.InputFileBig(file_id, part_count, file_name)
else:
return custom.InputSizedFile(
file_id, part_count, file_name, md5=hash_md5, size=file_size
)
# endregion
async def _file_to_media(
self, file, force_document=False,
progress_callback=None, attributes=None, thumb=None,
allow_cache=True, voice_note=False, video_note=False,
supports_streaming=False, mime_type=None, as_image=None):
if not file:
return None, None, None
if isinstance(file, pathlib.Path):
file = str(file.absolute())
if as_image is None:
as_image = utils.is_image(file) and not force_document
if not isinstance(file, (str, bytes, io.IOBase)):
# The user may pass a Message containing media (or the media,
# or anything similar) that should be treated as a file. Try
# getting the input media for whatever they passed and send it.
#
# We pass all attributes since these will be used if the user
# passed :tl:`InputFile`, and all information may be relevant.
try:
return (None, utils.get_input_media(
file,
is_photo=as_image,
attributes=attributes,
force_document=force_document,
voice_note=voice_note,
video_note=video_note,
supports_streaming=supports_streaming
), as_image)
except TypeError:
# Can't turn whatever was given into media
return None, None, as_image
media = None
file_handle = None
use_cache = types.InputPhoto if as_image else types.InputDocument
if not isinstance(file, str) or os.path.isfile(file):
file_handle = await self.upload_file(
_resize_photo_if_needed(file, as_image),
progress_callback=progress_callback,
use_cache=use_cache if allow_cache else None
)
elif re.match('https?://', file):
if as_image:
media = types.InputMediaPhotoExternal(file)
elif not force_document and utils.is_gif(file):
media = types.InputMediaGifExternal(file, '')
else:
media = types.InputMediaDocumentExternal(file)
else:
bot_file = utils.resolve_bot_file_id(file)
if bot_file:
media = utils.get_input_media(bot_file)
if media:
pass # Already have media, don't check the rest
elif not file_handle:
raise ValueError(
'Failed to convert {} to media. Not an existing file, '
'an HTTP URL or a valid bot-API-like file ID'.format(file)
)
elif isinstance(file_handle, use_cache):
# File was cached, so an instance of use_cache was returned
if as_image:
media = types.InputMediaPhoto(file_handle)
else:
media = types.InputMediaDocument(file_handle)
elif as_image:
media = types.InputMediaUploadedPhoto(file_handle)
else:
attributes, mime_type = utils.get_attributes(
file,
mime_type=mime_type,
attributes=attributes,
force_document=force_document,
voice_note=voice_note,
video_note=video_note,
supports_streaming=supports_streaming
)
input_kw = {}
if thumb:
if isinstance(thumb, pathlib.Path):
thumb = str(thumb.absolute())
input_kw['thumb'] = await self.upload_file(thumb)
media = types.InputMediaUploadedDocument(
file=file_handle,
mime_type=mime_type,
attributes=attributes,
**input_kw
)
return file_handle, media, as_image
async def _cache_media(self: 'TelegramClient', msg, file, file_handle, image):
if file and msg and isinstance(file_handle,
custom.InputSizedFile):
# There was a response message and we didn't use cached
# version, so cache whatever we just sent to the database.
md5, size = file_handle.md5, file_handle.size
if image:
to_cache = utils.get_input_photo(msg.media.photo)
else:
to_cache = utils.get_input_document(msg.media.document)
self.session.cache_file(md5, size, to_cache)
# endregion

View File

@ -1,554 +0,0 @@
import asyncio
import itertools
import time
import typing
from .. import errors, utils, hints
from ..errors import MultiError, RPCError
from ..helpers import retry_range
from ..tl import TLRequest, types, functions
_NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!')
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
class UserMethods:
async def __call__(self: 'TelegramClient', request, ordered=False):
requests = (request if utils.is_list_like(request) else (request,))
for r in requests:
if not isinstance(r, TLRequest):
raise _NOT_A_REQUEST()
await r.resolve(self, utils)
# Avoid making the request if it's already in a flood wait
if r.CONSTRUCTOR_ID in self._flood_waited_requests:
due = self._flood_waited_requests[r.CONSTRUCTOR_ID]
diff = round(due - time.time())
if diff <= 3: # Flood waits below 3 seconds are "ignored"
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
elif diff <= self.flood_sleep_threshold:
self._log[__name__].info(
'Sleeping early for %ds on flood wait', diff)
await asyncio.sleep(diff, loop=self._loop)
self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)
else:
raise errors.FloodWaitError(request=r, capture=diff)
request_index = 0
self._last_request = time.time()
for attempt in retry_range(self._request_retries):
try:
future = self._sender.send(request, ordered=ordered)
if isinstance(future, list):
results = []
exceptions = []
for f in future:
try:
result = await f
except RPCError as e:
exceptions.append(e)
results.append(None)
continue
self.session.process_entities(result)
self._entity_cache.add(result)
exceptions.append(None)
results.append(result)
request_index += 1
if any(x is not None for x in exceptions):
raise MultiError(exceptions, results, requests)
else:
return results
else:
result = await future
self.session.process_entities(result)
self._entity_cache.add(result)
return result
except (errors.ServerError, errors.RpcCallFailError,
errors.RpcMcgetFailError) as e:
self._log[__name__].warning(
'Telegram is having internal issues %s: %s',
e.__class__.__name__, e)
await asyncio.sleep(2)
except (errors.FloodWaitError, errors.FloodTestPhoneWaitError) as e:
if utils.is_list_like(request):
request = request[request_index]
self._flood_waited_requests\
[request.CONSTRUCTOR_ID] = time.time() + e.seconds
if e.seconds <= self.flood_sleep_threshold:
self._log[__name__].info('Sleeping for %ds on flood wait',
e.seconds)
await asyncio.sleep(e.seconds, loop=self._loop)
else:
raise
except (errors.PhoneMigrateError, errors.NetworkMigrateError,
errors.UserMigrateError) as e:
self._log[__name__].info('Phone migrated to %d', e.new_dc)
should_raise = isinstance(e, (
errors.PhoneMigrateError, errors.NetworkMigrateError
))
if should_raise and await self.is_user_authorized():
raise
await self._switch_dc(e.new_dc)
raise ValueError('Request was unsuccessful {} time(s)'
.format(attempt))
# region Public methods
async def get_me(self: 'TelegramClient', input_peer: bool = False) \
-> 'typing.Union[types.User, types.InputPeerUser]':
"""
Gets "me", the current :tl:`User` who is logged in.
If the user has not logged in yet, this method returns ``None``.
Arguments
input_peer (`bool`, optional):
Whether to return the :tl:`InputPeerUser` version or the normal
:tl:`User`. This can be useful if you just need to know the ID
of yourself.
Returns
Your own :tl:`User`.
Example
.. code-block:: python
print(client.get_me().username)
"""
if input_peer and self._self_input_peer:
return self._self_input_peer
try:
me = (await self(
functions.users.GetUsersRequest([types.InputUserSelf()])))[0]
self._bot = me.bot
if not self._self_input_peer:
self._self_input_peer = utils.get_input_peer(
me, allow_self=False
)
return self._self_input_peer if input_peer else me
except errors.UnauthorizedError:
return None
async def is_bot(self: 'TelegramClient') -> bool:
"""
Return ``True`` if the signed-in user is a bot, ``False`` otherwise.
Example
.. code-block:: python
if client.is_bot():
print('Beep')
else:
print('Hello')
"""
if self._bot is None:
self._bot = (await self.get_me()).bot
return self._bot
async def is_user_authorized(self: 'TelegramClient') -> bool:
"""
Returns ``True`` if the user is authorized (i.e. has logged in).
Example
.. code-block:: python
if not client.is_user_authorized():
client.send_code_request(phone)
code = input('enter code: ')
client.sign_in(phone, code)
"""
if self._authorized is None:
try:
# Any request that requires authorization will work
await self(functions.updates.GetStateRequest())
self._authorized = True
except errors.RPCError:
self._authorized = False
return self._authorized
async def get_entity(
self: 'TelegramClient',
entity: 'hints.EntitiesLike') -> 'hints.Entity':
"""
Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat`
or :tl:`Channel`. You can also pass a list or iterable of entities,
and they will be efficiently fetched from the network.
Arguments
entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
If a username is given, **the username will be resolved** making
an API call every time. Resolving usernames is an expensive
operation and will start hitting flood waits around 50 usernames
in a short period of time.
If you want to get the entity for a *cached* username, you should
first `get_input_entity(username) <get_input_entity>` which will
use the cache), and then use `get_entity` with the result of the
previous call.
Similar limits apply to invite links, and you should use their
ID instead.
Using phone numbers (from people in your contact list), exact
names, integer IDs or :tl:`Peer` rely on a `get_input_entity`
first, which in turn needs the entity to be in cache, unless
a :tl:`InputPeer` was passed.
Unsupported types will raise ``TypeError``.
If the entity can't be found, ``ValueError`` will be raised.
Returns
:tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the
input entity. A list will be returned if more than one was given.
Example
.. code-block:: python
from telethon import utils
me = client.get_entity('me')
print(utils.get_display_name(me))
chat = client.get_input_entity('username')
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.
for message in client.iter_messages('username'):
...
# Note that for this to work the phone number must be in your contacts
some_id = client.get_peer_id('+34123456789')
"""
single = not utils.is_list_like(entity)
if single:
entity = (entity,)
# Group input entities by string (resolve username),
# input users (get users), input chat (get chats) and
# input channels (get channels) to get the most entities
# in the less amount of calls possible.
inputs = []
for x in entity:
if isinstance(x, str):
inputs.append(x)
else:
inputs.append(await self.get_input_entity(x))
users = [x for x in inputs
if isinstance(x, (types.InputPeerUser, types.InputPeerSelf))]
chats = [x.chat_id for x in inputs
if isinstance(x, types.InputPeerChat)]
channels = [x for x in inputs
if isinstance(x, types.InputPeerChannel)]
if users:
# GetUsersRequest has a limit of 200 per call
tmp = []
while users:
curr, users = users[:200], users[200:]
tmp.extend(await self(functions.users.GetUsersRequest(curr)))
users = tmp
if chats: # TODO Handle chats slice?
chats = (await self(
functions.messages.GetChatsRequest(chats))).chats
if channels:
channels = (await self(
functions.channels.GetChannelsRequest(channels))).chats
# Merge users, chats and channels into a single dictionary
id_entity = {
utils.get_peer_id(x): x
for x in itertools.chain(users, chats, channels)
}
# We could check saved usernames and put them into the users,
# chats and channels list from before. While this would reduce
# the amount of ResolveUsername calls, it would fail to catch
# username changes.
result = []
for x in inputs:
if isinstance(x, str):
result.append(await self._get_entity_from_string(x))
elif not isinstance(x, types.InputPeerSelf):
result.append(id_entity[utils.get_peer_id(x)])
else:
result.append(next(
u for u in id_entity.values()
if isinstance(u, types.User) and u.is_self
))
return result[0] if single else result
async def get_input_entity(
self: 'TelegramClient',
peer: 'hints.EntityLike') -> 'types.TypeInputPeer':
"""
Turns the given entity into its input entity version.
Most requests use this kind of :tl:`InputPeer`, so this is the most
suitable call to make for those cases. **Generally you should let the
library do its job** and don't worry about getting the input entity
first, but if you're going to use an entity often, consider making the
call:
Arguments
entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
If a username or invite link is given, **the library will
use the cache**. This means that it's possible to be using
a username that *changed* or an old invite link (this only
happens if an invite link for a small group chat is used
after it was upgraded to a mega-group).
If the username or ID from the invite link is not found in
the cache, it will be fetched. The same rules apply to phone
numbers (``'+34 123456789'``) from people in your contact list.
If an exact name is given, it must be in the cache too. This
is not reliable as different people can share the same name
and which entity is returned is arbitrary, and should be used
only for quick tests.
If a positive integer ID is given, the entity will be searched
in cached users, chats or channels, without making any call.
If a negative integer ID is given, the entity will be searched
exactly as either a chat (prefixed with ``-``) or as a channel
(prefixed with ``-100``).
If a :tl:`Peer` is given, it will be searched exactly in the
cache as either a user, chat or channel.
If the given object can be turned into an input entity directly,
said operation will be done.
Unsupported types will raise ``TypeError``.
If the entity can't be found, ``ValueError`` will be raised.
Returns
:tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`
or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``.
If you need to get the ID of yourself, you should use
`get_me` with ``input_peer=True``) instead.
Example
.. code-block:: python
# If you're going to use "username" often in your code
# (make a lot of calls), consider getting its input entity
# once, and then using the "user" everywhere instead.
user = client.get_input_entity('username')
# The same applies to IDs, chats or channels.
chat = 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._entity_cache[peer]
except (AttributeError, KeyError):
pass
# Then come known strings that take precedence
if peer in ('me', 'self'):
return types.InputPeerSelf()
# No InputPeer, cached peer, or known string. Fetch from disk cache
try:
return self.session.get_input_entity(peer)
except ValueError:
pass
# Only network left to try
if isinstance(peer, str):
return utils.get_input_peer(
await self._get_entity_from_string(peer))
# If we're a bot and the user has messaged us privately users.getUsers
# will work with access_hash = 0. Similar for channels.getChannels.
# If we're not a bot but the user is in our contacts, it seems to work
# regardless. These are the only two special-cased requests.
peer = utils.get_peer(peer)
if isinstance(peer, types.PeerUser):
users = await self(functions.users.GetUsersRequest([
types.InputUser(peer.user_id, access_hash=0)]))
if users and not isinstance(users[0], types.UserEmpty):
# If the user passed a valid ID they expect to work for
# channels but would be valid for users, we get UserEmpty.
# Avoid returning the invalid empty input peer for that.
#
# We *could* try to guess if it's a channel first, and if
# it's not, work as a chat and try to validate it through
# another request, but that becomes too much work.
return utils.get_input_peer(users[0])
elif isinstance(peer, types.PeerChat):
return types.InputPeerChat(peer.chat_id)
elif isinstance(peer, types.PeerChannel):
try:
channels = await self(functions.channels.GetChannelsRequest([
types.InputChannel(peer.channel_id, access_hash=0)]))
return utils.get_input_peer(channels.chats[0])
except errors.ChannelInvalidError:
pass
raise ValueError(
'Could not find the input entity for {!r}. Please read https://'
'docs.telethon.dev/en/latest/concepts/entities.html to'
' find out more details.'
.format(peer)
)
async def get_peer_id(
self: 'TelegramClient',
peer: 'hints.EntityLike',
add_mark: bool = True) -> int:
"""
Gets the ID for the given entity.
This method needs to be ``async`` because `peer` supports usernames,
invite-links, phone numbers (from people in your contact list), etc.
If ``add_mark is False``, then a positive ID will be returned
instead. By default, bot-API style IDs (signed) are returned.
Example
.. code-block:: python
print(client.get_peer_id('me'))
"""
if isinstance(peer, int):
return utils.get_peer_id(peer, add_mark=add_mark)
try:
if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6):
# 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer'
peer = await self.get_input_entity(peer)
except AttributeError:
peer = await self.get_input_entity(peer)
if isinstance(peer, types.InputPeerSelf):
peer = await self.get_me(input_peer=True)
return utils.get_peer_id(peer, add_mark=add_mark)
# endregion
# region Private methods
async def _get_entity_from_string(self: 'TelegramClient', string):
"""
Gets a full entity from the given string, which may be a phone or
a username, and processes all the found entities on the session.
The string may also be a user link, or a channel/chat invite link.
This method has the side effect of adding the found users to the
session database, so it can be queried later without API calls,
if this option is enabled on the session.
Returns the found entity, or raises TypeError if not found.
"""
phone = utils.parse_phone(string)
if phone:
try:
for user in (await self(
functions.contacts.GetContactsRequest(0))).users:
if user.phone == phone:
return user
except errors.BotMethodInvalidError:
raise ValueError('Cannot get entity by phone number as a '
'bot (try using integer IDs, not strings)')
elif string.lower() in ('me', 'self'):
return await self.get_me()
else:
username, is_join_chat = utils.parse_username(string)
if is_join_chat:
invite = await self(
functions.messages.CheckChatInviteRequest(username))
if isinstance(invite, types.ChatInvite):
raise ValueError(
'Cannot get entity from a channel (or group) '
'that you are not part of. Join the group and retry'
)
elif isinstance(invite, types.ChatInviteAlready):
return invite.chat
elif username:
try:
result = await self(
functions.contacts.ResolveUsernameRequest(username))
except errors.UsernameNotOccupiedError as e:
raise ValueError('No user has "{}" as username'
.format(username)) from e
try:
pid = utils.get_peer_id(result.peer, add_mark=False)
if isinstance(result.peer, types.PeerUser):
return next(x for x in result.users if x.id == pid)
else:
return next(x for x in result.chats if x.id == pid)
except StopIteration:
pass
try:
# Nobody with this username, maybe it's an exact name/title
return await self.get_entity(
self.session.get_input_entity(string))
except ValueError:
pass
raise ValueError(
'Cannot find any entity corresponding to "{}"'.format(string)
)
async def _get_input_dialog(self: 'TelegramClient', dialog):
"""
Returns a :tl:`InputDialogPeer`. This is a bit tricky because
it may or not need access to the client to convert what's given
into an input entity.
"""
try:
if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer')
dialog.peer = await self.get_input_entity(dialog.peer)
return dialog
elif dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer')
return types.InputDialogPeer(dialog)
except AttributeError:
pass
return types.InputDialogPeer(await self.get_input_entity(dialog))
async def _get_input_notify(self: 'TelegramClient', notify):
"""
Returns a :tl:`InputNotifyPeer`. This is a bit tricky because
it may or not need access to the client to convert what's given
into an input entity.
"""
try:
if notify.SUBCLASS_OF_ID == 0x58981615:
if isinstance(notify, types.InputNotifyPeer):
notify.peer = await self.get_input_entity(notify.peer)
return notify
except AttributeError:
return types.InputNotifyPeer(await self.get_input_entity(notify))
# endregion

View File

@ -1,10 +0,0 @@
"""
This module contains several utilities regarding cryptographic purposes,
such as the AES IGE mode used by Telegram, the authorization key bound with
their data centers, and so on.
"""
from .aes import AES
from .aesctr import AESModeCTR
from .authkey import AuthKey
from .factorization import Factorization
from .cdndecrypter import CdnDecrypter

View File

@ -1,111 +0,0 @@
"""
AES IGE implementation in Python.
If available, cryptg will be used instead, otherwise
if available, libssl will be used instead, otherwise
the Python implementation will be used.
"""
import os
import pyaes
import logging
from . import libssl
__log__ = logging.getLogger(__name__)
try:
import cryptg
__log__.info('cryptg detected, it will be used for encryption')
except ImportError:
cryptg = None
if libssl.encrypt_ige and libssl.decrypt_ige:
__log__.info('libssl detected, it will be used for encryption')
else:
__log__.info('cryptg module not installed and libssl not found, '
'falling back to (slower) Python encryption')
class AES:
"""
Class that servers as an interface to encrypt and decrypt
text through the AES IGE mode.
"""
@staticmethod
def decrypt_ige(cipher_text, key, iv):
"""
Decrypts the given text in 16-bytes blocks by using the
given key and 32-bytes initialization vector.
"""
if cryptg:
return cryptg.decrypt_ige(cipher_text, key, iv)
if libssl.decrypt_ige:
return libssl.decrypt_ige(cipher_text, key, iv)
iv1 = iv[:len(iv) // 2]
iv2 = iv[len(iv) // 2:]
aes = pyaes.AES(key)
plain_text = []
blocks_count = len(cipher_text) // 16
cipher_text_block = [0] * 16
for block_index in range(blocks_count):
for i in range(16):
cipher_text_block[i] = \
cipher_text[block_index * 16 + i] ^ iv2[i]
plain_text_block = aes.decrypt(cipher_text_block)
for i in range(16):
plain_text_block[i] ^= iv1[i]
iv1 = cipher_text[block_index * 16:block_index * 16 + 16]
iv2 = plain_text_block
plain_text.extend(plain_text_block)
return bytes(plain_text)
@staticmethod
def encrypt_ige(plain_text, key, iv):
"""
Encrypts the given text in 16-bytes blocks by using the
given key and 32-bytes initialization vector.
"""
padding = len(plain_text) % 16
if padding:
plain_text += os.urandom(16 - padding)
if cryptg:
return cryptg.encrypt_ige(plain_text, key, iv)
if libssl.encrypt_ige:
return libssl.encrypt_ige(plain_text, key, iv)
iv1 = iv[:len(iv) // 2]
iv2 = iv[len(iv) // 2:]
aes = pyaes.AES(key)
cipher_text = []
blocks_count = len(plain_text) // 16
for block_index in range(blocks_count):
plain_text_block = list(
plain_text[block_index * 16:block_index * 16 + 16]
)
for i in range(16):
plain_text_block[i] ^= iv1[i]
cipher_text_block = aes.encrypt(plain_text_block)
for i in range(16):
cipher_text_block[i] ^= iv2[i]
iv1 = cipher_text_block
iv2 = plain_text[block_index * 16:block_index * 16 + 16]
cipher_text.extend(cipher_text_block)
return bytes(cipher_text)

View File

@ -1,42 +0,0 @@
"""
This module holds the AESModeCTR wrapper class.
"""
import pyaes
class AESModeCTR:
"""Wrapper around pyaes.AESModeOfOperationCTR mode with custom IV"""
# TODO Maybe make a pull request to pyaes to support iv on CTR
def __init__(self, key, iv):
"""
Initializes the AES CTR mode with the given key/iv pair.
:param key: the key to be used as bytes.
:param iv: the bytes initialization vector. Must have a length of 16.
"""
# TODO Use libssl if available
assert isinstance(key, bytes)
self._aes = pyaes.AESModeOfOperationCTR(key)
assert isinstance(iv, bytes)
assert len(iv) == 16
self._aes._counter._counter = list(iv)
def encrypt(self, data):
"""
Encrypts the given plain text through AES CTR.
:param data: the plain text to be encrypted.
:return: the encrypted cipher text.
"""
return self._aes.encrypt(data)
def decrypt(self, data):
"""
Decrypts the given cipher text through AES CTR
:param data: the cipher text to be decrypted.
:return: the decrypted plain text.
"""
return self._aes.decrypt(data)

View File

@ -1,63 +0,0 @@
"""
This module holds the AuthKey class.
"""
import struct
from hashlib import sha1
from ..extensions import BinaryReader
class AuthKey:
"""
Represents an authorization key, used to encrypt and decrypt
messages sent to Telegram's data centers.
"""
def __init__(self, data):
"""
Initializes a new authorization key.
:param data: the data in bytes that represent this auth key.
"""
self.key = data
@property
def key(self):
return self._key
@key.setter
def key(self, value):
if not value:
self._key = self.aux_hash = self.key_id = None
return
if isinstance(value, type(self)):
self._key, self.aux_hash, self.key_id = \
value._key, value.aux_hash, value.key_id
return
self._key = value
with BinaryReader(sha1(self._key).digest()) as reader:
self.aux_hash = reader.read_long(signed=False)
reader.read(4)
self.key_id = reader.read_long(signed=False)
# TODO This doesn't really fit here, it's only used in authentication
def calc_new_nonce_hash(self, new_nonce, number):
"""
Calculates the new nonce hash based on the current attributes.
:param new_nonce: the new nonce to be hashed.
:param number: number to prepend before the hash.
:return: the hash for the given new nonce.
"""
new_nonce = new_nonce.to_bytes(32, 'little', signed=True)
data = new_nonce + struct.pack('<BQ', number, self.aux_hash)
# Calculates the message key from the given data
return int.from_bytes(sha1(data).digest()[4:20], 'little', signed=True)
def __bool__(self):
return bool(self._key)
def __eq__(self, other):
return isinstance(other, type(self)) and other.key == self._key

View File

@ -1,105 +0,0 @@
"""
This module holds the CdnDecrypter utility class.
"""
from hashlib import sha256
from ..tl.functions.upload import GetCdnFileRequest, ReuploadCdnFileRequest
from ..tl.types.upload import CdnFileReuploadNeeded, CdnFile
from ..crypto import AESModeCTR
from ..errors import CdnFileTamperedError
class CdnDecrypter:
"""
Used when downloading a file results in a 'FileCdnRedirect' to
both prepare the redirect, decrypt the file as it downloads, and
ensure the file hasn't been tampered. https://core.telegram.org/cdn
"""
def __init__(self, cdn_client, file_token, cdn_aes, cdn_file_hashes):
"""
Initializes the CDN decrypter.
:param cdn_client: a client connected to a CDN.
:param file_token: the token of the file to be used.
:param cdn_aes: the AES CTR used to decrypt the file.
:param cdn_file_hashes: the hashes the decrypted file must match.
"""
self.client = cdn_client
self.file_token = file_token
self.cdn_aes = cdn_aes
self.cdn_file_hashes = cdn_file_hashes
@staticmethod
async def prepare_decrypter(client, cdn_client, cdn_redirect):
"""
Prepares a new CDN decrypter.
:param client: a TelegramClient connected to the main servers.
:param cdn_client: a new client connected to the CDN.
:param cdn_redirect: the redirect file object that caused this call.
:return: (CdnDecrypter, first chunk file data)
"""
cdn_aes = AESModeCTR(
key=cdn_redirect.encryption_key,
# 12 first bytes of the IV..4 bytes of the offset (0, big endian)
iv=cdn_redirect.encryption_iv[:12] + bytes(4)
)
# We assume that cdn_redirect.cdn_file_hashes are ordered by offset,
# and that there will be enough of these to retrieve the whole file.
decrypter = CdnDecrypter(
cdn_client, cdn_redirect.file_token,
cdn_aes, cdn_redirect.cdn_file_hashes
)
cdn_file = await cdn_client(GetCdnFileRequest(
file_token=cdn_redirect.file_token,
offset=cdn_redirect.cdn_file_hashes[0].offset,
limit=cdn_redirect.cdn_file_hashes[0].limit
))
if isinstance(cdn_file, CdnFileReuploadNeeded):
# We need to use the original client here
await client(ReuploadCdnFileRequest(
file_token=cdn_redirect.file_token,
request_token=cdn_file.request_token
))
# We want to always return a valid upload.CdnFile
cdn_file = decrypter.get_file()
else:
cdn_file.bytes = decrypter.cdn_aes.encrypt(cdn_file.bytes)
cdn_hash = decrypter.cdn_file_hashes.pop(0)
decrypter.check(cdn_file.bytes, cdn_hash)
return decrypter, cdn_file
def get_file(self):
"""
Calls GetCdnFileRequest and decrypts its bytes.
Also ensures that the file hasn't been tampered.
:return: the CdnFile result.
"""
if self.cdn_file_hashes:
cdn_hash = self.cdn_file_hashes.pop(0)
cdn_file = self.client(GetCdnFileRequest(
self.file_token, cdn_hash.offset, cdn_hash.limit
))
cdn_file.bytes = self.cdn_aes.encrypt(cdn_file.bytes)
self.check(cdn_file.bytes, cdn_hash)
else:
cdn_file = CdnFile(bytes(0))
return cdn_file
@staticmethod
def check(data, cdn_hash):
"""
Checks the integrity of the given data.
Raises CdnFileTamperedError if the integrity check fails.
:param data: the data to be hashed.
:param cdn_hash: the expected hash.
"""
if sha256(data).digest() != cdn_hash.hash:
raise CdnFileTamperedError()

View File

@ -1,65 +0,0 @@
"""
This module holds a fast Factorization class.
"""
from random import randint
class Factorization:
"""
Simple module to factorize large numbers really quickly.
"""
@classmethod
def factorize(cls, pq):
"""
Factorizes the given large integer.
:param pq: the prime pair pq.
:return: a tuple containing the two factors p and q.
"""
if pq % 2 == 0:
return 2, pq // 2
y, c, m = randint(1, pq - 1), randint(1, pq - 1), randint(1, pq - 1)
g = r = q = 1
x = ys = 0
while g == 1:
x = y
for i in range(r):
y = (pow(y, 2, pq) + c) % pq
k = 0
while k < r and g == 1:
ys = y
for i in range(min(m, r - k)):
y = (pow(y, 2, pq) + c) % pq
q = q * (abs(x - y)) % pq
g = cls.gcd(q, pq)
k += m
r *= 2
if g == pq:
while True:
ys = (pow(ys, 2, pq) + c) % pq
g = cls.gcd(abs(x - ys), pq)
if g > 1:
break
p, q = g, pq // g
return (p, q) if p < q else (q, p)
@staticmethod
def gcd(a, b):
"""
Calculates the Greatest Common Divisor.
:param a: the first number.
:param b: the second number.
:return: GCD(a, b)
"""
while b:
a, b = b, a % b
return a

View File

@ -1,122 +0,0 @@
"""
Helper module around the system's libssl library if available for IGE mode.
"""
import ctypes
import ctypes.util
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')
if not lib:
raise OSError('no library called "ssl" found')
# First, let ctypes try to handle it itself.
try:
libssl = ctypes.cdll.LoadLibrary(lib)
except OSError:
pass
else:
return libssl
# This is a best-effort attempt at finding the full real path of lib.
#
# Unfortunately ctypes doesn't tell us *where* it finds the library,
# so we have to do that ourselves.
try:
# This is not documented, so it could fail. Be on the safe side.
paths = ctypes.macholib.dyld.DEFAULT_LIBRARY_FALLBACK
except AttributeError:
paths = [
os.path.expanduser("~/lib"),
"/usr/local/lib",
"/lib",
"/usr/lib",
]
for path in paths:
if os.path.isdir(path):
for root, _, files in os.walk(path):
if lib in files:
# Manually follow symbolic links on *nix systems.
# Fix for https://github.com/LonamiWebs/Telethon/issues/1167
lib = os.path.realpath(os.path.join(root, lib))
return ctypes.cdll.LoadLibrary(lib)
else:
raise OSError('no absolute path for "%s" and cannot load by name' % lib)
try:
_libssl = _find_ssl_lib()
except OSError as e:
# See https://github.com/LonamiWebs/Telethon/issues/1167
# Sometimes `find_library` returns improper filenames.
__log__.info('Failed to load SSL library: %s (%s)', type(e), e)
_libssl = None
if not _libssl:
decrypt_ige = None
encrypt_ige = None
else:
# https://github.com/openssl/openssl/blob/master/include/openssl/aes.h
AES_ENCRYPT = ctypes.c_int(1)
AES_DECRYPT = ctypes.c_int(0)
AES_MAXNR = 14
class AES_KEY(ctypes.Structure):
"""Helper class representing an AES key"""
_fields_ = [
('rd_key', ctypes.c_uint32 * (4 * (AES_MAXNR + 1))),
('rounds', ctypes.c_uint),
]
def decrypt_ige(cipher_text, key, iv):
aes_key = AES_KEY()
key_len = ctypes.c_int(8 * len(key))
key = (ctypes.c_ubyte * len(key))(*key)
iv = (ctypes.c_ubyte * len(iv))(*iv)
in_len = ctypes.c_size_t(len(cipher_text))
in_ptr = (ctypes.c_ubyte * len(cipher_text))(*cipher_text)
out_ptr = (ctypes.c_ubyte * len(cipher_text))()
_libssl.AES_set_decrypt_key(key, key_len, ctypes.byref(aes_key))
_libssl.AES_ige_encrypt(
ctypes.byref(in_ptr),
ctypes.byref(out_ptr),
in_len,
ctypes.byref(aes_key),
ctypes.byref(iv),
AES_DECRYPT
)
return bytes(out_ptr)
def encrypt_ige(plain_text, key, iv):
aes_key = AES_KEY()
key_len = ctypes.c_int(8 * len(key))
key = (ctypes.c_ubyte * len(key))(*key)
iv = (ctypes.c_ubyte * len(iv))(*iv)
in_len = ctypes.c_size_t(len(plain_text))
in_ptr = (ctypes.c_ubyte * len(plain_text))(*plain_text)
out_ptr = (ctypes.c_ubyte * len(plain_text))()
_libssl.AES_set_encrypt_key(key, key_len, ctypes.byref(aes_key))
_libssl.AES_ige_encrypt(
ctypes.byref(in_ptr),
ctypes.byref(out_ptr),
in_len,
ctypes.byref(aes_key),
ctypes.byref(iv),
AES_ENCRYPT
)
return bytes(out_ptr)

View File

@ -1,122 +0,0 @@
"""
This module holds several utilities regarding RSA and server fingerprints.
"""
import os
import struct
from hashlib import sha1
try:
import rsa
import rsa.core
except ImportError:
rsa = None
raise ImportError('Missing module "rsa", please install via pip.')
from ..tl import TLObject
# {fingerprint: Crypto.PublicKey.RSA._RSAobj} dictionary
_server_keys = {}
def get_byte_array(integer):
"""Return the variable length bytes corresponding to the given int"""
# Operate in big endian (unlike most of Telegram API) since:
# > "...pq is a representation of a natural number
# (in binary *big endian* format)..."
# > "...current value of dh_prime equals
# (in *big-endian* byte order)..."
# Reference: https://core.telegram.org/mtproto/auth_key
return int.to_bytes(
integer,
(integer.bit_length() + 8 - 1) // 8, # 8 bits per byte,
byteorder='big',
signed=False
)
def _compute_fingerprint(key):
"""
Given a RSA key, computes its fingerprint like Telegram does.
:param key: the Crypto.RSA key.
:return: its 8-bytes-long fingerprint.
"""
n = TLObject.serialize_bytes(get_byte_array(key.n))
e = TLObject.serialize_bytes(get_byte_array(key.e))
# Telegram uses the last 8 bytes as the fingerprint
return struct.unpack('<q', sha1(n + e).digest()[-8:])[0]
def add_key(pub):
"""Adds a new public key to be used when encrypting new data is needed"""
global _server_keys
key = rsa.PublicKey.load_pkcs1(pub)
_server_keys[_compute_fingerprint(key)] = key
def encrypt(fingerprint, data):
"""
Encrypts the given data known the fingerprint to be used
in the way Telegram requires us to do so (sha1(data) + data + padding)
:param fingerprint: the fingerprint of the RSA key.
:param data: the data to be encrypted.
:return:
the cipher text, or None if no key matching this fingerprint is found.
"""
global _server_keys
key = _server_keys.get(fingerprint, None)
if not key:
return None
# len(sha1.digest) is always 20, so we're left with 255 - 20 - x padding
to_encrypt = sha1(data).digest() + data + os.urandom(235 - len(data))
# rsa module rsa.encrypt adds 11 bits for padding which we don't want
# rsa module uses rsa.transform.bytes2int(to_encrypt), easier way:
payload = int.from_bytes(to_encrypt, 'big')
encrypted = rsa.core.encrypt_int(payload, key.e, key.n)
# rsa module uses transform.int2bytes(encrypted, keylength), easier:
block = encrypted.to_bytes(256, 'big')
return block
# Add default keys
for pub in (
'''-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS
an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw
Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+
8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n
Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB
-----END RSA PUBLIC KEY-----''',
'''-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAxq7aeLAqJR20tkQQMfRn+ocfrtMlJsQ2Uksfs7Xcoo77jAid0bRt
ksiVmT2HEIJUlRxfABoPBV8wY9zRTUMaMA654pUX41mhyVN+XoerGxFvrs9dF1Ru
vCHbI02dM2ppPvyytvvMoefRoL5BTcpAihFgm5xCaakgsJ/tH5oVl74CdhQw8J5L
xI/K++KJBUyZ26Uba1632cOiq05JBUW0Z2vWIOk4BLysk7+U9z+SxynKiZR3/xdi
XvFKk01R3BHV+GUKM2RYazpS/P8v7eyKhAbKxOdRcFpHLlVwfjyM1VlDQrEZxsMp
NTLYXb6Sce1Uov0YtNx5wEowlREH1WOTlwIDAQAB
-----END RSA PUBLIC KEY-----''',
'''-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAsQZnSWVZNfClk29RcDTJQ76n8zZaiTGuUsi8sUhW8AS4PSbPKDm+
DyJgdHDWdIF3HBzl7DHeFrILuqTs0vfS7Pa2NW8nUBwiaYQmPtwEa4n7bTmBVGsB
1700/tz8wQWOLUlL2nMv+BPlDhxq4kmJCyJfgrIrHlX8sGPcPA4Y6Rwo0MSqYn3s
g1Pu5gOKlaT9HKmE6wn5Sut6IiBjWozrRQ6n5h2RXNtO7O2qCDqjgB2vBxhV7B+z
hRbLbCmW0tYMDsvPpX5M8fsO05svN+lKtCAuz1leFns8piZpptpSCFn7bWxiA9/f
x5x17D7pfah3Sy2pA+NDXyzSlGcKdaUmwQIDAQAB
-----END RSA PUBLIC KEY-----''',
'''-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAwqjFW0pi4reKGbkc9pK83Eunwj/k0G8ZTioMMPbZmW99GivMibwa
xDM9RDWabEMyUtGoQC2ZcDeLWRK3W8jMP6dnEKAlvLkDLfC4fXYHzFO5KHEqF06i
qAqBdmI1iBGdQv/OQCBcbXIWCGDY2AsiqLhlGQfPOI7/vvKc188rTriocgUtoTUc
/n/sIUzkgwTqRyvWYynWARWzQg0I9olLBBC2q5RQJJlnYXZwyTL3y9tdb7zOHkks
WV9IMQmZmyZh/N7sMbGWQpt4NMchGpPGeJ2e5gHBjDnlIf2p1yZOYeUYrdbwcS0t
UiggS4UeE8TzIuXFQxw7fzEIlmhIaq3FnwIDAQAB
-----END RSA PUBLIC KEY-----'''
):
add_key(pub)

View File

@ -1,141 +0,0 @@
import inspect
import itertools
from . import utils
from .tl import types
# Which updates have the following fields?
_has_field = {
('user_id', int): [],
('chat_id', int): [],
('channel_id', int): [],
('peer', 'TypePeer'): [],
('peer', 'TypeDialogPeer'): [],
('message', 'TypeMessage'): [],
}
# Note: We don't bother checking for some rare:
# * `UpdateChatParticipantAdd.inviter_id` integer.
# * `UpdateNotifySettings.peer` dialog peer.
# * `UpdatePinnedDialogs.order` list of dialog peers.
# * `UpdateReadMessagesContents.messages` list of messages.
# * `UpdateChatParticipants.participants` list of participants.
#
# There are also some uninteresting `update.message` of type string.
def _fill():
for name in dir(types):
update = getattr(types, name)
if getattr(update, 'SUBCLASS_OF_ID', None) == 0x9f89304e:
cid = update.CONSTRUCTOR_ID
sig = inspect.signature(update.__init__)
for param in sig.parameters.values():
vec = _has_field.get((param.name, param.annotation))
if vec is not None:
vec.append(cid)
# Future-proof check: if the documentation format ever changes
# then we won't be able to pick the update types we are interested
# in, so we must make sure we have at least an update for each field
# which likely means we are doing it right.
if not all(_has_field.values()):
raise RuntimeError('FIXME: Did the init signature or updates change?')
# We use a function to avoid cluttering the globals (with name/update/cid/doc)
_fill()
class EntityCache:
"""
In-memory input entity cache, defaultdict-like behaviour.
"""
def add(self, entities):
"""
Adds the given entities to the cache, if they weren't saved before.
"""
if not utils.is_list_like(entities):
# Invariant: all "chats" and "users" are always iterables,
# and "user" never is (so we wrap it inside a list).
entities = itertools.chain(
getattr(entities, 'chats', []),
getattr(entities, 'users', []),
(hasattr(entities, 'user') and [entities.user]) or []
)
for entity in entities:
try:
pid = utils.get_peer_id(entity)
if pid not in self.__dict__:
# Note: `get_input_peer` already checks for `access_hash`
self.__dict__[pid] = utils.get_input_peer(entity)
except TypeError:
pass
def __getitem__(self, item):
"""
Gets the corresponding :tl:`InputPeer` for the given ID or peer,
or raises ``KeyError`` on any error (i.e. cannot be found).
"""
if not isinstance(item, int) or item < 0:
try:
return self.__dict__[utils.get_peer_id(item)]
except TypeError:
raise KeyError('Invalid key will not have entity') from None
for cls in (types.PeerUser, types.PeerChat, types.PeerChannel):
result = self.__dict__.get(utils.get_peer_id(cls(item)))
if result:
return result
raise KeyError('No cached entity for the given key')
def ensure_cached(
self,
update,
has_user_id=frozenset(_has_field[('user_id', int)]),
has_chat_id=frozenset(_has_field[('chat_id', int)]),
has_channel_id=frozenset(_has_field[('channel_id', int)]),
has_peer=frozenset(_has_field[('peer', 'TypePeer')] + _has_field[('peer', 'TypeDialogPeer')]),
has_message=frozenset(_has_field[('message', 'TypeMessage')])
):
"""
Ensures that all the relevant entities in the given update are cached.
"""
# This method is called pretty often and we want it to have the lowest
# overhead possible. For that, we avoid `isinstance` and constantly
# getting attributes out of `types.` by "caching" the constructor IDs
# in sets inside the arguments, and using local variables.
dct = self.__dict__
cid = update.CONSTRUCTOR_ID
if cid in has_user_id and \
update.user_id not in dct:
return False
if cid in has_chat_id and \
utils.get_peer_id(types.PeerChat(update.chat_id)) not in dct:
return False
if cid in has_channel_id and \
utils.get_peer_id(types.PeerChannel(update.channel_id)) not in dct:
return False
if cid in has_peer and \
utils.get_peer_id(update.peer) not in dct:
return False
if cid in has_message:
x = update.message
y = getattr(x, 'to_id', None) # handle MessageEmpty
if y and utils.get_peer_id(y) not in dct:
return False
y = getattr(x, 'from_id', None)
if y and y not in dct:
return False
# We don't quite worry about entities anywhere else.
# This is enough.
return True

View File

@ -1,45 +0,0 @@
"""
This module holds all the base and automatically generated errors that the
Telegram API has. See telethon_generator/errors.json for more.
"""
import re
from .common import (
ReadCancelledError, TypeNotFoundError, InvalidChecksumError,
InvalidBufferError, SecurityError, CdnFileTamperedError,
AlreadyInConversationError, BadMessageError, MultiError
)
# This imports the base errors too, as they're imported there
from .rpcbaseerrors import *
from .rpcerrorlist import *
def rpc_message_to_error(rpc_error, request):
"""
Converts a Telegram's RPC Error to a Python error.
:param rpc_error: the RpcError instance.
:param request: the request that caused this error.
:return: the RPCError as a Python exception that represents this error.
"""
# Try to get the error by direct look-up, otherwise regex
cls = rpc_errors_dict.get(rpc_error.error_message, None)
if cls:
return cls(request=request)
for msg_regex, cls in rpc_errors_re:
m = re.match(msg_regex, rpc_error.error_message)
if m:
capture = int(m.group(1)) if m.groups() else None
return cls(request=request, capture=capture)
# Some errors are negative:
# * -500 for "No workers running",
# * -503 for "Timeout"
#
# We treat them as if they were positive, so -500 will be treated
# as a `ServerError`, etc.
cls = base_errors.get(abs(rpc_error.error_code), RPCError)
return cls(request=request, message=rpc_error.error_message,
code=rpc_error.error_code)

View File

@ -1,163 +0,0 @@
"""Errors not related to the Telegram API itself"""
import struct
from ..tl import TLRequest
class ReadCancelledError(Exception):
"""Occurs when a read operation was cancelled."""
def __init__(self):
super().__init__('The read operation was cancelled.')
class TypeNotFoundError(Exception):
"""
Occurs when a type is not found, for example,
when trying to read a TLObject with an invalid constructor code.
"""
def __init__(self, invalid_constructor_id, remaining):
super().__init__(
'Could not find a matching Constructor ID for the TLObject '
'that was supposed to be read with ID {:08x}. Most likely, '
'a TLObject was trying to be read when it should not be read. '
'Remaining bytes: {!r}'.format(invalid_constructor_id, remaining))
self.invalid_constructor_id = invalid_constructor_id
self.remaining = remaining
class InvalidChecksumError(Exception):
"""
Occurs when using the TCP full mode and the checksum of a received
packet doesn't match the expected checksum.
"""
def __init__(self, checksum, valid_checksum):
super().__init__(
'Invalid checksum ({} when {} was expected). '
'This packet should be skipped.'
.format(checksum, valid_checksum))
self.checksum = checksum
self.valid_checksum = valid_checksum
class InvalidBufferError(BufferError):
"""
Occurs when the buffer is invalid, and may contain an HTTP error code.
For instance, 404 means "forgotten/broken authorization key", while
"""
def __init__(self, payload):
self.payload = payload
if len(payload) == 4:
self.code = -struct.unpack('<i', payload)[0]
super().__init__(
'Invalid response buffer (HTTP code {})'.format(self.code))
else:
self.code = None
super().__init__(
'Invalid response buffer (too short {})'.format(self.payload))
class SecurityError(Exception):
"""
Generic security error, mostly used when generating a new AuthKey.
"""
def __init__(self, *args):
if not args:
args = ['A security check failed.']
super().__init__(*args)
class CdnFileTamperedError(SecurityError):
"""
Occurs when there's a hash mismatch between the decrypted CDN file
and its expected hash.
"""
def __init__(self):
super().__init__(
'The CDN file has been altered and its download cancelled.'
)
class AlreadyInConversationError(Exception):
"""
Occurs when another exclusive conversation is opened in the same chat.
"""
def __init__(self):
super().__init__(
'Cannot open exclusive conversation in a '
'chat that already has one open conversation'
)
class BadMessageError(Exception):
"""Occurs when handling a bad_message_notification."""
ErrorMessages = {
16:
'msg_id too low (most likely, client time is wrong it would be '
'worthwhile to synchronize it using msg_id notifications and re-send '
'the original message with the "correct" msg_id or wrap it in a '
'container with a new msg_id if the original message had waited too '
'long on the client to be transmitted).',
17:
'msg_id too high (similar to the previous case, the client time has '
'to be synchronized, and the message re-sent with the correct msg_id).',
18:
'Incorrect two lower order msg_id bits (the server expects client '
'message msg_id to be divisible by 4).',
19:
'Container msg_id is the same as msg_id of a previously received '
'message (this must never happen).',
20:
'Message too old, and it cannot be verified whether the server has '
'received a message with this msg_id or not.',
32:
'msg_seqno too low (the server has already received a message with a '
'lower msg_id but with either a higher or an equal and odd seqno).',
33:
'msg_seqno too high (similarly, there is a message with a higher '
'msg_id but with either a lower or an equal and odd seqno).',
34:
'An even msg_seqno expected (irrelevant message), but odd received.',
35:
'Odd msg_seqno expected (relevant message), but even received.',
48:
'Incorrect server salt (in this case, the bad_server_salt response '
'is received with the correct salt, and the message is to be re-sent '
'with it).',
64:
'Invalid container.'
}
def __init__(self, request, code):
super().__init__(request, self.ErrorMessages.get(
code,
'Unknown error code (this should not happen): {}.'.format(code)))
self.code = code
class MultiError(Exception):
"""Exception container for multiple `TLRequest`'s."""
def __new__(cls, exceptions, result, requests):
if len(result) != len(exceptions) != len(requests):
raise ValueError(
'Need result, exception and request for each error')
for e, req in zip(exceptions, requests):
if not isinstance(e, BaseException) and e is not None:
raise TypeError(
"Expected an exception object, not '%r'" % e
)
if not isinstance(req, TLRequest):
raise TypeError(
"Expected TLRequest object, not '%r'" % req
)
if len(exceptions) == 1:
return exceptions[0]
self = BaseException.__new__(cls)
self.exceptions = list(exceptions)
self.results = list(result)
self.requests = list(requests)
return self

View File

@ -1,110 +0,0 @@
class RPCError(Exception):
"""Base class for all Remote Procedure Call errors."""
code = None
message = None
def __init__(self, request, message, code=None):
super().__init__('RPCError {}: {}{}'.format(
code or self.code, message, self._fmt_request(request)))
self.code = code
self.message = message
@staticmethod
def _fmt_request(request):
return ' (caused by {})'.format(request.__class__.__name__)
def __reduce__(self):
return type(self), (self.code, self.message)
class InvalidDCError(RPCError):
"""
The request must be repeated, but directed to a different data center.
"""
code = 303
message = 'ERROR_SEE_OTHER'
class BadRequestError(RPCError):
"""
The query contains errors. In the event that a request was created
using a form and contains user generated data, the user should be
notified that the data must be corrected before the query is repeated.
"""
code = 400
message = 'BAD_REQUEST'
class UnauthorizedError(RPCError):
"""
There was an unauthorized attempt to use functionality available only
to authorized users.
"""
code = 401
message = 'UNAUTHORIZED'
class ForbiddenError(RPCError):
"""
Privacy violation. For example, an attempt to write a message to
someone who has blacklisted the current user.
"""
code = 403
message = 'FORBIDDEN'
class NotFoundError(RPCError):
"""
An attempt to invoke a non-existent object, such as a method.
"""
code = 404
message = 'NOT_FOUND'
class AuthKeyError(RPCError):
"""
Errors related to invalid authorization key, like
AUTH_KEY_DUPLICATED which can cause the connection to fail.
"""
code = 406
message = 'AUTH_KEY'
class FloodError(RPCError):
"""
The maximum allowed number of attempts to invoke the given method
with the given input parameters has been exceeded. For example, in an
attempt to request a large number of text messages (SMS) for the same
phone number.
"""
code = 420
message = 'FLOOD'
class ServerError(RPCError):
"""
An internal server error occurred while a request was being processed
for example, there was a disruption while accessing a database or file
storage.
"""
code = 500 # Also witnessed as -500
message = 'INTERNAL'
class TimedOutError(RPCError):
"""
Clicking the inline buttons of bots that never (or take to long to)
call ``answerCallbackQuery`` will result in this "special" RPCError.
"""
code = 503 # Only witnessed as -503
message = 'Timeout'
BotTimeout = TimedOutError
base_errors = {x.code: x for x in (
InvalidDCError, BadRequestError, UnauthorizedError, ForbiddenError,
NotFoundError, AuthKeyError, FloodError, ServerError, TimedOutError
)}

View File

@ -1,139 +0,0 @@
from .raw import Raw
from .chataction import ChatAction
from .messagedeleted import MessageDeleted
from .messageedited import MessageEdited
from .messageread import MessageRead
from .newmessage import NewMessage
from .userupdate import UserUpdate
from .callbackquery import CallbackQuery
from .inlinequery import InlineQuery
_HANDLERS_ATTRIBUTE = '__tl.handlers'
class StopPropagation(Exception):
"""
If this exception is raised in any of the handlers for a given event,
it will stop the execution of all other registered event handlers.
It can be seen as the ``StopIteration`` in a for loop but for events.
Example usage:
>>> from telethon import TelegramClient, events
>>> client = TelegramClient(...)
>>>
>>> @client.on(events.NewMessage)
... async def delete(event):
... await event.delete()
... # No other event handler will have a chance to handle this event
... raise StopPropagation
...
>>> @client.on(events.NewMessage)
... async def _(event):
... # Will never be reached, because it is the second handler
... pass
"""
# For some reason Sphinx wants the silly >>> or
# it will show warnings and look bad when generated.
pass
def register(event=None):
"""
Decorator method to *register* event handlers. This is the client-less
`add_event_handler()
<telethon.client.updates.UpdateMethods.add_event_handler>` variant.
Note that this method only registers callbacks as handlers,
and does not attach them to any client. This is useful for
external modules that don't have access to the client, but
still want to define themselves as a handler. Example:
>>> from telethon import events
>>> @events.register(events.NewMessage)
... async def handler(event):
... ...
...
>>> # (somewhere else)
...
>>> from telethon import TelegramClient
>>> client = TelegramClient(...)
>>> client.add_event_handler(handler)
Remember that you can use this as a non-decorator
through ``register(event)(callback)``.
Args:
event (`_EventBuilder` | `type`):
The event builder class or instance to be used,
for instance ``events.NewMessage``.
"""
if isinstance(event, type):
event = event()
elif not event:
event = Raw()
def decorator(callback):
handlers = getattr(callback, _HANDLERS_ATTRIBUTE, [])
handlers.append(event)
setattr(callback, _HANDLERS_ATTRIBUTE, handlers)
return callback
return decorator
def unregister(callback, event=None):
"""
Inverse operation of `register` (though not a decorator). Client-less
`remove_event_handler
<telethon.client.updates.UpdateMethods.remove_event_handler>`
variant. **Note that this won't remove handlers from the client**,
because it simply can't, so you would generally use this before
adding the handlers to the client.
This method is here for symmetry. You will rarely need to
unregister events, since you can simply just not add them
to any client.
If no event is given, all events for this callback are removed.
Returns how many callbacks were removed.
"""
found = 0
if event and not isinstance(event, type):
event = type(event)
handlers = getattr(callback, _HANDLERS_ATTRIBUTE, [])
handlers.append((event, callback))
i = len(handlers)
while i:
i -= 1
ev = handlers[i]
if not event or isinstance(ev, event):
del handlers[i]
found += 1
return found
def is_handler(callback):
"""
Returns ``True`` if the given callback is an
event handler (i.e. you used `register` on it).
"""
return hasattr(callback, _HANDLERS_ATTRIBUTE)
def list(callback):
"""
Returns a list containing the registered event
builders inside the specified callback handler.
"""
return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:]
def _get_handlers(callback):
"""
Like ``list`` but returns ``None`` if the callback was never registered.
"""
return getattr(callback, _HANDLERS_ATTRIBUTE, None)

View File

@ -1,297 +0,0 @@
import re
import struct
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types, functions
from ..tl.custom.sendergetter import SenderGetter
@name_inner_event
class CallbackQuery(EventBuilder):
"""
Occurs whenever you sign in as a bot and a user
clicks one of the inline buttons on your messages.
Note that the `chats` parameter will **not** work with normal
IDs or peers if the clicked inline button comes from a "via bot"
message. The `chats` parameter also supports checking against the
`chat_instance` which should be used for inline callbacks.
Args:
data (`bytes` | `str` | `callable`, optional):
If set, the inline button payload data must match this data.
A UTF-8 string can also be given, a regex or a callable. For
instance, to check against ``'data_1'`` and ``'data_2'`` you
can use ``re.compile(b'data_')``.
"""
def __init__(
self, chats=None, *, blacklist_chats=False, func=None, data=None):
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
if isinstance(data, bytes):
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 & (~re.UNICODE))
self.data = data.match
else:
raise TypeError('Invalid data type given')
@classmethod
def build(cls, update):
if isinstance(update, types.UpdateBotCallbackQuery):
event = cls.Event(update, update.peer, update.msg_id)
elif isinstance(update, types.UpdateInlineBotCallbackQuery):
# See https://github.com/LonamiWebs/Telethon/pull/1005
# The long message ID is actually just msg_id + peer_id
mid, pid = struct.unpack('<ii', struct.pack('<q', update.msg_id.id))
peer = types.PeerChannel(-pid) if pid < 0 else types.PeerUser(pid)
event = cls.Event(update, peer, mid)
else:
return
event._entities = update._entities
return event
def filter(self, event):
# We can't call super().filter(...) because it ignores chat_instance
if self.chats is not None:
inside = event.query.chat_instance in self.chats
if event.chat_id:
inside |= event.chat_id in self.chats
if inside == self.blacklist_chats:
return None
if self.data:
if callable(self.data):
event.data_match = self.data(event.query.data)
if not event.data_match:
return None
elif event.query.data != self.data:
return None
if not self.func or self.func(event):
return event
class Event(EventCommon, SenderGetter):
"""
Represents the event of a new callback query.
Members:
query (:tl:`UpdateBotCallbackQuery`):
The original :tl:`UpdateBotCallbackQuery`.
data_match (`obj`, optional):
The object returned by the ``data=`` parameter
when creating the event builder, if any. Similar
to ``pattern_match`` for the new message event.
"""
def __init__(self, query, peer, msg_id):
super().__init__(peer, msg_id=msg_id)
SenderGetter.__init__(self, query.user_id)
self.query = query
self.data_match = None
self._message = None
self._answered = False
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache)
@property
def id(self):
"""
Returns the query ID. The user clicking the inline
button is the one who generated this random ID.
"""
return self.query.query_id
@property
def message_id(self):
"""
Returns the message ID to which the clicked inline button belongs.
"""
return self._message_id
@property
def data(self):
"""
Returns the data payload from the original inline button.
"""
return self.query.data
@property
def chat_instance(self):
"""
Unique identifier for the chat where the callback occurred.
Useful for high scores in games.
"""
return self.query.chat_instance
async def get_message(self):
"""
Returns the message to which the clicked inline button belongs.
"""
if self._message is not None:
return self._message
try:
chat = await self.get_input_chat() if self.is_channel else None
self._message = await self._client.get_messages(
chat, ids=self._message_id)
except ValueError:
return
return self._message
async def _refetch_sender(self):
self._sender = self._entities.get(self.sender_id)
if not self._sender:
return
self._input_sender = utils.get_input_peer(self._chat)
if not getattr(self._input_sender, 'access_hash', True):
# getattr with True to handle the InputPeerSelf() case
try:
self._input_sender = self._client._entity_cache[self._sender_id]
except KeyError:
m = await self.get_message()
if m:
self._sender = m._sender
self._input_sender = m._input_sender
async def answer(
self, message=None, cache_time=0, *, url=None, alert=False):
"""
Answers the callback query (and stops the loading circle).
Args:
message (`str`, optional):
The toast message to show feedback to the user.
cache_time (`int`, optional):
For how long this result should be cached on
the user's client. Defaults to 0 for no cache.
url (`str`, optional):
The URL to be opened in the user's client. Note that
the only valid URLs are those of games your bot has,
or alternatively a 't.me/your_bot?start=xyz' parameter.
alert (`bool`, optional):
Whether an alert (a pop-up dialog) should be used
instead of showing a toast. Defaults to ``False``.
"""
if self._answered:
return
self._answered = True
return await self._client(
functions.messages.SetBotCallbackAnswerRequest(
query_id=self.query.query_id,
cache_time=cache_time,
alert=alert,
message=message,
url=url
)
)
@property
def via_inline(self):
"""
Whether this callback was generated from an inline button sent
via an inline query or not. If the bot sent the message itself
with buttons, and one of those is clicked, this will be ``False``.
If a user sent the message coming from an inline query to the
bot, and one of those is clicked, this will be ``True``.
If it's ``True``, it's likely that the bot is **not** in the
chat, so methods like `respond` or `delete` won't work (but
`edit` will always work).
"""
return isinstance(self.query, types.UpdateInlineBotCallbackQuery)
async def respond(self, *args, **kwargs):
"""
Responds to the message (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
``entity`` already set.
This method also creates a task to `answer` the callback.
This method will likely fail if `via_inline` is ``True``.
"""
self._client.loop.create_task(self.answer())
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
async def reply(self, *args, **kwargs):
"""
Replies to the message (as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
both ``entity`` and ``reply_to`` already set.
This method also creates a task to `answer` the callback.
This method will likely fail if `via_inline` is ``True``.
"""
self._client.loop.create_task(self.answer())
kwargs['reply_to'] = self.query.msg_id
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
async def edit(self, *args, **kwargs):
"""
Edits the message. Shorthand for
`telethon.client.messages.MessageMethods.edit_message` with
the ``entity`` set to the correct :tl:`InputBotInlineMessageID`.
Returns ``True`` if the edit was successful.
This method also creates a task to `answer` the callback.
.. note::
This method won't respect the previous message unlike
`Message.edit <telethon.tl.custom.message.Message.edit>`,
since the message object is normally not present.
"""
self._client.loop.create_task(self.answer())
if isinstance(self.query.msg_id, types.InputBotInlineMessageID):
return await self._client.edit_message(
self.query.msg_id, *args, **kwargs
)
else:
return await self._client.edit_message(
await self.get_input_chat(), self.query.msg_id,
*args, **kwargs
)
async def delete(self, *args, **kwargs):
"""
Deletes the message. Shorthand for
`telethon.client.messages.MessageMethods.delete_messages` with
``entity`` and ``message_ids`` already set.
If you need to delete more than one message at once, don't use
this `delete` method. Use a
`telethon.client.telegramclient.TelegramClient` instance directly.
This method also creates a task to `answer` the callback.
This method will likely fail if `via_inline` is ``True``.
"""
self._client.loop.create_task(self.answer())
return await self._client.delete_messages(
await self.get_input_chat(), [self.query.msg_id],
*args, **kwargs
)

View File

@ -1,390 +0,0 @@
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types, functions
@name_inner_event
class ChatAction(EventBuilder):
"""
Occurs whenever a user joins or leaves a chat, or a message is pinned.
"""
@classmethod
def build(cls, update):
if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0:
# Telegram does not always send
# UpdateChannelPinnedMessage for new pins
# but always for unpin, with update.id = 0
event = cls.Event(types.PeerChannel(update.channel_id),
unpin=True)
elif isinstance(update, types.UpdateChatParticipantAdd):
event = cls.Event(types.PeerChat(update.chat_id),
added_by=update.inviter_id or True,
users=update.user_id)
elif isinstance(update, types.UpdateChatParticipantDelete):
event = cls.Event(types.PeerChat(update.chat_id),
kicked_by=True,
users=update.user_id)
elif (isinstance(update, (
types.UpdateNewMessage, types.UpdateNewChannelMessage))
and isinstance(update.message, types.MessageService)):
msg = update.message
action = update.message.action
if isinstance(action, types.MessageActionChatJoinedByLink):
event = cls.Event(msg,
added_by=True,
users=msg.from_id)
elif isinstance(action, types.MessageActionChatAddUser):
# If a user adds itself, it means they joined
added_by = ([msg.from_id] == action.users) or msg.from_id
event = cls.Event(msg,
added_by=added_by,
users=action.users)
elif isinstance(action, types.MessageActionChatDeleteUser):
event = cls.Event(msg,
kicked_by=msg.from_id or True,
users=action.user_id)
elif isinstance(action, types.MessageActionChatCreate):
event = cls.Event(msg,
users=action.users,
created=True,
new_title=action.title)
elif isinstance(action, types.MessageActionChannelCreate):
event = cls.Event(msg,
created=True,
users=msg.from_id,
new_title=action.title)
elif isinstance(action, types.MessageActionChatEditTitle):
event = cls.Event(msg,
users=msg.from_id,
new_title=action.title)
elif isinstance(action, types.MessageActionChatEditPhoto):
event = cls.Event(msg,
users=msg.from_id,
new_photo=action.photo)
elif isinstance(action, types.MessageActionChatDeletePhoto):
event = cls.Event(msg,
users=msg.from_id,
new_photo=True)
elif isinstance(action, types.MessageActionPinMessage):
# Telegram always sends this service message for new pins
event = cls.Event(msg,
users=msg.from_id,
new_pin=msg.reply_to_msg_id)
else:
return
else:
return
event._entities = update._entities
return event
class Event(EventCommon):
"""
Represents the event of a new chat action.
Members:
action_message (`MessageAction <https://tl.telethon.dev/types/message_action.html>`_):
The message invoked by this Chat Action.
new_pin (`bool`):
``True`` if there is a new pin.
new_photo (`bool`):
``True`` if there's a new chat photo (or it was removed).
photo (:tl:`Photo`, optional):
The new photo (or ``None`` if it was removed).
user_added (`bool`):
``True`` if the user was added by some other.
user_joined (`bool`):
``True`` if the user joined on their own.
user_left (`bool`):
``True`` if the user left on their own.
user_kicked (`bool`):
``True`` if the user was kicked by some other.
created (`bool`, optional):
``True`` if this chat was just created.
new_title (`str`, optional):
The new title string for the chat, if applicable.
unpin (`bool`):
``True`` if the existing pin gets unpinned.
"""
def __init__(self, where, new_pin=None, new_photo=None,
added_by=None, kicked_by=None, created=None,
users=None, new_title=None, unpin=None):
if isinstance(where, types.MessageService):
self.action_message = where
where = where.to_id
else:
self.action_message = None
super().__init__(chat_peer=where, msg_id=new_pin)
self.new_pin = isinstance(new_pin, int)
self._pinned_message = new_pin
self.new_photo = new_photo is not None
self.photo = \
new_photo if isinstance(new_photo, types.Photo) else None
self._added_by = None
self._kicked_by = None
self.user_added = self.user_joined = self.user_left = \
self.user_kicked = self.unpin = False
if added_by is True:
self.user_joined = True
elif added_by:
self.user_added = True
self._added_by = added_by
# If `from_id` was not present (it's ``True``) or the affected
# user was "kicked by itself", then it left. Else it was kicked.
if kicked_by is True or kicked_by == users:
self.user_left = True
elif kicked_by:
self.user_kicked = True
self._kicked_by = kicked_by
self.created = bool(created)
self._user_peers = users if isinstance(users, list) else [users]
self._users = None
self._input_users = None
self.new_title = new_title
self.unpin = unpin
def _set_client(self, client):
super()._set_client(client)
if self.action_message:
self.action_message._finish_init(client, self._entities, None)
async def respond(self, *args, **kwargs):
"""
Responds to the chat action message (not as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
``entity`` already set.
"""
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
async def reply(self, *args, **kwargs):
"""
Replies to the chat action message (as a reply). Shorthand for
`telethon.client.messages.MessageMethods.send_message` with
both ``entity`` and ``reply_to`` already set.
Has the same effect as `respond` if there is no message.
"""
if not self.action_message:
return await self.respond(*args, **kwargs)
kwargs['reply_to'] = self.action_message.id
return await self._client.send_message(
await self.get_input_chat(), *args, **kwargs)
async def delete(self, *args, **kwargs):
"""
Deletes the chat action message. You're responsible for checking
whether you have the permission to do so, or to except the error
otherwise. Shorthand for
`telethon.client.messages.MessageMethods.delete_messages` with
``entity`` and ``message_ids`` already set.
Does nothing if no message action triggered this event.
"""
if not self.action_message:
return
return await self._client.delete_messages(
await self.get_input_chat(), [self.action_message],
*args, **kwargs
)
async def get_pinned_message(self):
"""
If ``new_pin`` is ``True``, this returns the `Message
<telethon.tl.custom.message.Message>` object that was pinned.
"""
if self._pinned_message == 0:
return None
if isinstance(self._pinned_message, int)\
and await self.get_input_chat():
r = await 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
if isinstance(self._pinned_message, types.Message):
return self._pinned_message
@property
def added_by(self):
"""
The user who added ``users``, if applicable (``None`` otherwise).
"""
if self._added_by and not isinstance(self._added_by, types.User):
aby = self._entities.get(utils.get_peer_id(self._added_by))
if aby:
self._added_by = aby
return self._added_by
async def get_added_by(self):
"""
Returns `added_by` but will make an API call if necessary.
"""
if not self.added_by and self._added_by:
self._added_by = await self._client.get_entity(self._added_by)
return self._added_by
@property
def kicked_by(self):
"""
The user who kicked ``users``, if applicable (``None`` otherwise).
"""
if self._kicked_by and not isinstance(self._kicked_by, types.User):
kby = self._entities.get(utils.get_peer_id(self._kicked_by))
if kby:
self._kicked_by = kby
return self._kicked_by
async def get_kicked_by(self):
"""
Returns `kicked_by` but will make an API call if necessary.
"""
if not self.kicked_by and self._kicked_by:
self._kicked_by = await self._client.get_entity(self._kicked_by)
return self._kicked_by
@property
def user(self):
"""
The first user that takes part in this action (e.g. joined).
Might be ``None`` if the information can't be retrieved or
there is no user taking part.
"""
if self.users:
return self._users[0]
async def get_user(self):
"""
Returns `user` but will make an API call if necessary.
"""
if self.users or await self.get_users():
return self._users[0]
@property
def input_user(self):
"""
Input version of the ``self.user`` property.
"""
if self.input_users:
return self._input_users[0]
async def get_input_user(self):
"""
Returns `input_user` but will make an API call if necessary.
"""
if self.input_users or await self.get_input_users():
return self._input_users[0]
@property
def user_id(self):
"""
Returns the marked signed ID of the first user, if any.
"""
if self._user_peers:
return utils.get_peer_id(self._user_peers[0])
@property
def users(self):
"""
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
are no users taking part.
"""
if not self._user_peers:
return []
if self._users is None:
self._users = [
self._entities[utils.get_peer_id(peer)]
for peer in self._user_peers
if utils.get_peer_id(peer) in self._entities
]
return self._users
async def get_users(self):
"""
Returns `users` but will make an API call if necessary.
"""
if not self._user_peers:
return []
if self._users is None or len(self._users) != len(self._user_peers):
await self.action_message._reload_message()
self._users = [
u for u in self.action_message.action_entities
if isinstance(u, (types.User, types.UserEmpty))]
return self._users
@property
def input_users(self):
"""
Input version of the ``self.users`` property.
"""
if self._input_users is None and self._user_peers:
self._input_users = []
for peer in self._user_peers:
try:
self._input_users.append(self._client._entity_cache[peer])
except KeyError:
pass
return self._input_users or []
async def get_input_users(self):
"""
Returns `input_users` but will make an API call if necessary.
"""
self._input_users = None
if self._input_users is None:
await self.action_message._reload_message()
self._input_users = [
utils.get_input_peer(u)
for u in self.action_message.action_entities
if isinstance(u, (types.User, types.UserEmpty))]
return self._input_users or []
@property
def user_ids(self):
"""
Returns the marked signed ID of the users, if any.
"""
if self._user_peers:
return [utils.get_peer_id(u) for u in self._user_peers]

View File

@ -1,179 +0,0 @@
import abc
import asyncio
import itertools
import warnings
from .. import utils
from ..tl import TLObject, types, functions
from ..tl.custom.chatgetter import ChatGetter
async def _into_id_set(client, chats):
"""Helper util to turn the input chat or chats into a set of IDs."""
if chats is None:
return None
if not utils.is_list_like(chats):
chats = (chats,)
result = set()
for chat in chats:
if isinstance(chat, int):
if chat < 0:
result.add(chat) # Explicitly marked IDs are negative
else:
result.update({ # Support all valid types of peers
utils.get_peer_id(types.PeerUser(chat)),
utils.get_peer_id(types.PeerChat(chat)),
utils.get_peer_id(types.PeerChannel(chat)),
})
elif isinstance(chat, TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687:
# 0x2d45687 == crc32(b'Peer')
result.add(utils.get_peer_id(chat))
else:
chat = await client.get_input_entity(chat)
if isinstance(chat, types.InputPeerSelf):
chat = await client.get_me(input_peer=True)
result.add(utils.get_peer_id(chat))
return result
class EventBuilder(abc.ABC):
"""
The common event builder, with builtin support to filter per chat.
Args:
chats (`entity`, optional):
May be one or more entities (username/peer/etc.), preferably IDs.
By default, only matching chats will be handled.
blacklist_chats (`bool`, optional):
Whether to treat the chats as a blacklist instead of
as a whitelist (default). This means that every chat
will be handled *except* those specified in ``chats``
which will be ignored if ``blacklist_chats=True``.
func (`callable`, optional):
A callable 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
"""
self_id = None
def __init__(self, chats=None, *, blacklist_chats=False, func=None):
self.chats = chats
self.blacklist_chats = bool(blacklist_chats)
self.resolved = False
self.func = func
self._resolve_lock = None
@classmethod
@abc.abstractmethod
def build(cls, update):
"""Builds an event for the given update if possible, or returns None"""
async def resolve(self, client):
"""Helper method to allow event builders to be resolved before usage"""
if self.resolved:
return
if not self._resolve_lock:
self._resolve_lock = asyncio.Lock(loop=client.loop)
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)
if not EventBuilder.self_id:
EventBuilder.self_id = await client.get_peer_id('me')
def filter(self, event):
"""
If the ID of ``event._chat_peer`` isn't in the chats set (or it is
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 None
if self.chats is not None:
# Note: the `event.chat_id` property checks if it's `None` for us
inside = event.chat_id in self.chats
if inside == self.blacklist_chats:
# If this chat matches but it's a blacklist ignore.
# If it doesn't match but it's a whitelist ignore.
return None
if not self.func or self.func(event):
return event
class EventCommon(ChatGetter, abc.ABC):
"""
Intermediate class with common things to all events.
Remember that this class implements `ChatGetter
<telethon.tl.custom.chatgetter.ChatGetter>` which
means you have access to all chat properties and methods.
In addition, you can access the `original_update`
field which contains the original :tl:`Update`.
"""
_event_name = 'Event'
def __init__(self, chat_peer=None, msg_id=None, broadcast=None):
super().__init__(chat_peer, broadcast=broadcast)
self._entities = {}
self._client = None
self._message_id = msg_id
self.original_update = None
def _set_client(self, client):
"""
Setter so subclasses can act accordingly when the client is set.
"""
self._client = client
if self._chat_peer:
self._chat, self._input_chat = utils._get_entity_pair(
self.chat_id, self._entities, client._entity_cache)
else:
self._chat = self._input_chat = None
@property
def client(self):
"""
The `telethon.TelegramClient` that created this event.
"""
return self._client
def __str__(self):
return TLObject.pretty_format(self.to_dict())
def stringify(self):
return TLObject.pretty_format(self.to_dict(), indent=0)
def to_dict(self):
d = {k: v for k, v in self.__dict__.items() if k[0] != '_'}
d['_'] = self._event_name
return d
def name_inner_event(cls):
"""Decorator to rename cls.Event 'Event' as 'cls.Event'"""
if hasattr(cls, 'Event'):
cls.Event._event_name = '{}.Event'.format(cls.__name__)
else:
warnings.warn('Class {} does not have a inner Event'.format(cls))
return cls

View File

@ -1,235 +0,0 @@
import inspect
import re
import asyncio
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types, functions, custom
from ..tl.custom.sendergetter import SenderGetter
@name_inner_event
class InlineQuery(EventBuilder):
"""
Occurs whenever you sign in as a bot and a user
sends an inline query such as ``@bot query``.
Args:
users (`entity`, optional):
May be one or more entities (username/peer/etc.), preferably IDs.
By default, only inline queries from these users will be handled.
blacklist_users (`bool`, optional):
Whether to treat the users as a blacklist instead of
as a whitelist (default). This means that every chat
will be handled *except* those specified in ``users``
which will be ignored if ``blacklist_users=True``.
pattern (`str`, `callable`, `Pattern`, optional):
If set, only queries matching this pattern will be handled.
You can specify a regex-like string which will be matched
against the message, a callable function that returns ``True``
if a message is acceptable, or a compiled regex pattern.
"""
def __init__(
self, users=None, *, blacklist_users=False, func=None, pattern=None):
super().__init__(users, blacklist_chats=blacklist_users, func=func)
if isinstance(pattern, str):
self.pattern = re.compile(pattern).match
elif not pattern or callable(pattern):
self.pattern = pattern
elif hasattr(pattern, 'match') and callable(pattern.match):
self.pattern = pattern.match
else:
raise TypeError('Invalid pattern type given')
@classmethod
def build(cls, update):
if isinstance(update, types.UpdateBotInlineQuery):
event = cls.Event(update)
else:
return
event._entities = update._entities
return event
def filter(self, event):
if self.pattern:
match = self.pattern(event.text)
if not match:
return
event.pattern_match = match
return super().filter(event)
class Event(EventCommon, SenderGetter):
"""
Represents the event of a new callback query.
Members:
query (:tl:`UpdateBotCallbackQuery`):
The original :tl:`UpdateBotCallbackQuery`.
Make sure to access the `text` of the query if
that's what you want instead working with this.
pattern_match (`obj`, optional):
The resulting object from calling the passed ``pattern``
function, which is ``re.compile(...).match`` by default.
"""
def __init__(self, query):
super().__init__(chat_peer=types.PeerUser(query.user_id))
SenderGetter.__init__(self, query.user_id)
self.query = query
self.pattern_match = None
self._answered = False
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache)
@property
def id(self):
"""
Returns the unique identifier for the query ID.
"""
return self.query.query_id
@property
def text(self):
"""
Returns the text the user used to make the inline query.
"""
return self.query.query
@property
def offset(self):
"""
The string the user's client used as an offset for the query.
This will either be empty or equal to offsets passed to `answer`.
"""
return self.query.offset
@property
def geo(self):
"""
If the user location is requested when using inline mode
and the user's device is able to send it, this will return
the :tl:`GeoPoint` with the position of the user.
"""
return
@property
def builder(self):
"""
Returns a new `InlineBuilder
<telethon.tl.custom.inlinebuilder.InlineBuilder>` instance.
"""
return custom.InlineBuilder(self._client)
async def answer(
self, results=None, cache_time=0, *,
gallery=False, next_offset=None, private=False,
switch_pm=None, switch_pm_param=''):
"""
Answers the inline query with the given results.
Args:
results (`list`, optional):
A list of :tl:`InputBotInlineResult` to use.
You should use `builder` to create these:
.. code-block:: python
builder = inline.builder
r1 = builder.article('Be nice', text='Have a nice day')
r2 = builder.article('Be bad', text="I don't like you")
await inline.answer([r1, r2])
You can send up to 50 results as documented in
https://core.telegram.org/bots/api#answerinlinequery.
Sending more will raise ``ResultsTooMuchError``,
and you should consider using `next_offset` to
paginate them.
cache_time (`int`, optional):
For how long this result should be cached on
the user's client. Defaults to 0 for no cache.
gallery (`bool`, optional):
Whether the results should show as a gallery (grid) or not.
next_offset (`str`, optional):
The offset the client will send when the user scrolls the
results and it repeats the request.
private (`bool`, optional):
Whether the results should be cached by Telegram
(not private) or by the user's client (private).
switch_pm (`str`, optional):
If set, this text will be shown in the results
to allow the user to switch to private messages.
switch_pm_param (`str`, optional):
Optional parameter to start the bot with if
`switch_pm` was used.
Example:
.. code-block:: python
@bot.on(events.InlineQuery)
async def handler(event):
builder = event.builder
rev_text = event.text[::-1]
await event.answer([
builder.article('Reverse text', text=rev_text),
builder.photo('/path/to/photo.jpg')
])
"""
if self._answered:
return
if results:
futures = [self._as_future(x, self._client.loop)
for x in results]
await asyncio.wait(futures, loop=self._client.loop)
# All futures will be in the `done` *set* that `wait` returns.
#
# Precisely because it's a `set` and not a `list`, it
# will not preserve the order, but since all futures
# completed we can use our original, ordered `list`.
results = [x.result() for x in futures]
else:
results = []
if switch_pm:
switch_pm = types.InlineBotSwitchPM(switch_pm, switch_pm_param)
return await self._client(
functions.messages.SetInlineBotResultsRequest(
query_id=self.query.query_id,
results=results,
cache_time=cache_time,
gallery=gallery,
next_offset=next_offset,
private=private,
switch_pm=switch_pm
)
)
@staticmethod
def _as_future(obj, loop):
if inspect.isawaitable(obj):
return asyncio.ensure_future(obj, loop=loop)
f = loop.create_future()
f.set_result(obj)
return f

View File

@ -1,51 +0,0 @@
from .common import EventBuilder, EventCommon, name_inner_event
from ..tl import types
@name_inner_event
class MessageDeleted(EventBuilder):
"""
Occurs whenever a message is deleted. Note that this event isn't 100%
reliable, since Telegram doesn't always notify the clients that a message
was deleted.
.. important::
Telegram **does not** send information about *where* a message
was deleted if it occurs in private conversations with other users
or in small group chats, because message IDs are *unique* and you
can identify the chat with the message ID alone if you saved it
previously.
Telethon **does not** save information of where messages occur,
so it cannot know in which chat a message was deleted (this will
only work in channels, where the channel ID *is* present).
This means that the ``chats=`` parameter will not work reliably,
unless you intend on working with channels and super-groups only.
"""
@classmethod
def build(cls, update):
if isinstance(update, types.UpdateDeleteMessages):
event = cls.Event(
deleted_ids=update.messages,
peer=None
)
elif isinstance(update, types.UpdateDeleteChannelMessages):
event = cls.Event(
deleted_ids=update.messages,
peer=types.PeerChannel(update.channel_id)
)
else:
return
event._entities = update._entities
return event
class Event(EventCommon):
def __init__(self, deleted_ids, peer):
super().__init__(
chat_peer=peer, msg_id=(deleted_ids or [0])[0]
)
self.deleted_id = None if not deleted_ids else deleted_ids[0]
self.deleted_ids = deleted_ids

View File

@ -1,47 +0,0 @@
from .common import name_inner_event
from .newmessage import NewMessage
from ..tl import types
@name_inner_event
class MessageEdited(NewMessage):
"""
Occurs whenever a message is edited. Just like `NewMessage
<telethon.events.newmessage.NewMessage>`, you should treat
this event as a `Message <telethon.tl.custom.message.Message>`.
.. warning::
On channels, `Message.out <telethon.tl.custom.message.Message>`
will be ``True`` if you sent the message originally, **not if
you edited it**! This can be dangerous if you run outgoing
commands on edits.
Some examples follow:
* You send a message "A", ``out is True``.
* You edit "A" to "B", ``out is True``.
* Someone else edits "B" to "C", ``out is True`` (**be careful!**).
* Someone sends "X", ``out is False``.
* Someone edits "X" to "Y", ``out is False``.
* You edit "Y" to "Z", ``out is False``.
Since there are useful cases where you need the right ``out``
value, the library cannot do anything automatically to help you.
Instead, consider using ``from_users='me'`` (it won't work in
broadcast channels at all since the sender is the channel and
not you).
"""
@classmethod
def build(cls, update):
if isinstance(update, (types.UpdateEditMessage,
types.UpdateEditChannelMessage)):
event = cls.Event(update.message)
else:
return
event._entities = update._entities
return event
class Event(NewMessage.Event):
pass # Required if we want a different name for it

View File

@ -1,133 +0,0 @@
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types
@name_inner_event
class MessageRead(EventBuilder):
"""
Occurs whenever one or more messages are read in a chat.
Args:
inbox (`bool`, optional):
If this argument is ``True``, then when you read someone else's
messages the event will be fired. By default (``False``) only
when messages you sent are read by someone else will fire it.
"""
def __init__(
self, chats=None, *, blacklist_chats=False, func=None, inbox=False):
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
self.inbox = inbox
@classmethod
def build(cls, update):
if isinstance(update, types.UpdateReadHistoryInbox):
event = cls.Event(update.peer, update.max_id, False)
elif isinstance(update, types.UpdateReadHistoryOutbox):
event = cls.Event(update.peer, update.max_id, True)
elif isinstance(update, types.UpdateReadChannelInbox):
event = cls.Event(types.PeerChannel(update.channel_id),
update.max_id, False)
elif isinstance(update, types.UpdateReadChannelOutbox):
event = cls.Event(types.PeerChannel(update.channel_id),
update.max_id, True)
elif isinstance(update, types.UpdateReadMessagesContents):
event = cls.Event(message_ids=update.messages,
contents=True)
elif isinstance(update, types.UpdateChannelReadMessagesContents):
event = cls.Event(types.PeerChannel(update.channel_id),
message_ids=update.messages,
contents=True)
else:
return
event._entities = update._entities
return event
def filter(self, event):
if self.inbox == event.outbox:
return
return super().filter(event)
class Event(EventCommon):
"""
Represents the event of one or more messages being read.
Members:
max_id (`int`):
Up to which message ID has been read. Every message
with an ID equal or lower to it have been read.
outbox (`bool`):
``True`` if someone else has read your messages.
contents (`bool`):
``True`` if what was read were the contents of a message.
This will be the case when e.g. you play a voice note.
It may only be set on ``inbox`` events.
"""
def __init__(self, peer=None, max_id=None, out=False, contents=False,
message_ids=None):
self.outbox = out
self.contents = contents
self._message_ids = message_ids or []
self._messages = None
self.max_id = max_id or max(message_ids or [], default=None)
super().__init__(peer, self.max_id)
@property
def inbox(self):
"""
``True`` if you have read someone else's messages.
"""
return not self.outbox
@property
def message_ids(self):
"""
The IDs of the messages **which contents'** were read.
Use :meth:`is_read` if you need to check whether a message
was read instead checking if it's in here.
"""
return self._message_ids
async def get_messages(self):
"""
Returns the list of `Message <telethon.tl.custom.message.Message>`
**which contents'** were read.
Use :meth:`is_read` if you need to check whether a message
was read instead checking if it's in here.
"""
if self._messages is None:
chat = await self.get_input_chat()
if not chat:
self._messages = []
else:
self._messages = await self._client.get_messages(
chat, ids=self._message_ids)
return self._messages
def is_read(self, message):
"""
Returns ``True`` if the given message (or its ID) has been read.
If a list-like argument is provided, this method will return a
list of booleans indicating which messages have been read.
"""
if utils.is_list_like(message):
return [(m if isinstance(m, int) else m.id) <= self.max_id
for m in message]
else:
return (message if isinstance(message, int)
else message.id) <= self.max_id
def __contains__(self, message):
"""``True`` if the message(s) are read message."""
if utils.is_list_like(message):
return all(self.is_read(message))
else:
return self.is_read(message)

View File

@ -1,222 +0,0 @@
import asyncio
import re
from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set
from ..tl import types
@name_inner_event
class NewMessage(EventBuilder):
"""
Occurs whenever a new text message or a message with media arrives.
Args:
incoming (`bool`, optional):
If set to ``True``, only **incoming** messages will be handled.
Mutually exclusive with ``outgoing`` (can only set one of either).
outgoing (`bool`, optional):
If set to ``True``, only **outgoing** messages will be handled.
Mutually exclusive with ``incoming`` (can only set one of either).
from_users (`entity`, optional):
Unlike `chats`, this parameter filters the *senders* of the
message. That is, only messages *sent by these users* will be
handled. Use `chats` if you want private messages with this/these
users. `from_users` lets you filter by messages sent by *one or
more* users across the desired chats (doesn't need a list).
forwards (`bool`, optional):
Whether forwarded messages should be handled or not. By default,
both forwarded and normal messages are included. If it's ``True``
*only* forwards will be handled. If it's ``False`` only messages
that are *not* forwards will be handled.
pattern (`str`, `callable`, `Pattern`, optional):
If set, only messages matching this pattern will be handled.
You can specify a regex-like string which will be matched
against the message, a callable function that returns ``True``
if a message is acceptable, or a compiled regex pattern.
"""
def __init__(self, chats=None, *, blacklist_chats=False, func=None,
incoming=None, outgoing=None,
from_users=None, forwards=None, pattern=None):
if incoming and outgoing:
incoming = outgoing = None # Same as no filter
elif incoming is not None and outgoing is None:
outgoing = not incoming
elif outgoing is not None and incoming is None:
incoming = not outgoing
elif all(x is not None and not x for x in (incoming, outgoing)):
raise ValueError("Don't create an event handler if you "
"don't want neither incoming nor outgoing!")
super().__init__(chats, blacklist_chats=blacklist_chats, func=func)
self.incoming = incoming
self.outgoing = outgoing
self.from_users = from_users
self.forwards = forwards
if isinstance(pattern, str):
self.pattern = re.compile(pattern).match
elif not pattern or callable(pattern):
self.pattern = pattern
elif hasattr(pattern, 'match') and callable(pattern.match):
self.pattern = pattern.match
else:
raise TypeError('Invalid pattern type given')
# Should we short-circuit? E.g. perform no check at all
self._no_check = all(x is None for x in (
self.chats, self.incoming, self.outgoing, self.pattern,
self.from_users, self.forwards, self.from_users, self.func
))
async def _resolve(self, client):
await super()._resolve(client)
self.from_users = await _into_id_set(client, self.from_users)
@classmethod
def build(cls, update):
if isinstance(update,
(types.UpdateNewMessage, types.UpdateNewChannelMessage)):
if not isinstance(update.message, types.Message):
return # We don't care about MessageService's here
event = cls.Event(update.message)
elif isinstance(update, types.UpdateShortMessage):
event = cls.Event(types.Message(
out=update.out,
mentioned=update.mentioned,
media_unread=update.media_unread,
silent=update.silent,
id=update.id,
# Note that to_id/from_id complement each other in private
# messages, depending on whether the message was outgoing.
to_id=types.PeerUser(
update.user_id if update.out else cls.self_id
),
from_id=cls.self_id if update.out else update.user_id,
message=update.message,
date=update.date,
fwd_from=update.fwd_from,
via_bot_id=update.via_bot_id,
reply_to_msg_id=update.reply_to_msg_id,
entities=update.entities
))
elif isinstance(update, types.UpdateShortChatMessage):
event = cls.Event(types.Message(
out=update.out,
mentioned=update.mentioned,
media_unread=update.media_unread,
silent=update.silent,
id=update.id,
from_id=update.from_id,
to_id=types.PeerChat(update.chat_id),
message=update.message,
date=update.date,
fwd_from=update.fwd_from,
via_bot_id=update.via_bot_id,
reply_to_msg_id=update.reply_to_msg_id,
entities=update.entities
))
else:
return
# Make messages sent to ourselves outgoing unless they're forwarded.
# This makes it consistent with official client's appearance.
ori = event.message
if isinstance(ori.to_id, types.PeerUser):
if ori.from_id == ori.to_id.user_id and not ori.fwd_from:
event.message.out = True
event._entities = update._entities
return event
def filter(self, event):
if self._no_check:
return event
if self.incoming and event.message.out:
return
if self.outgoing and not event.message.out:
return
if self.forwards is not None:
if bool(self.forwards) != bool(event.message.fwd_from):
return
if self.from_users is not None:
if event.message.from_id not in self.from_users:
return
if self.pattern:
match = self.pattern(event.message.message or '')
if not match:
return
event.pattern_match = match
return super().filter(event)
class Event(EventCommon):
"""
Represents the event of a new message. This event can be treated
to all effects as a `Message <telethon.tl.custom.message.Message>`,
so please **refer to its documentation** to know what you can do
with this event.
Members:
message (`Message <telethon.tl.custom.message.Message>`):
This is the only difference with the received
`Message <telethon.tl.custom.message.Message>`, and will
return the `telethon.tl.custom.message.Message` itself,
not the text.
See `Message <telethon.tl.custom.message.Message>` for
the rest of available members and methods.
pattern_match (`obj`):
The resulting object from calling the passed ``pattern`` function.
Here's an example using a string (defaults to regex match):
>>> from telethon import TelegramClient, events
>>> client = TelegramClient(...)
>>>
>>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!'))
... async def handler(event):
... # In this case, the result is a ``Match`` object
... # since the ``str`` pattern was converted into
... # the ``re.compile(pattern).match`` function.
... print('Welcomed', event.pattern_match.group(1))
...
>>>
"""
def __init__(self, message):
self.__dict__['_init'] = False
if not message.out and isinstance(message.to_id, types.PeerUser):
# Incoming message (e.g. from a bot) has to_id=us, and
# from_id=bot (the actual "chat" from a user's perspective).
chat_peer = types.PeerUser(message.from_id)
else:
chat_peer = message.to_id
super().__init__(chat_peer=chat_peer,
msg_id=message.id, broadcast=bool(message.post))
self.pattern_match = None
self.message = message
def _set_client(self, client):
super()._set_client(client)
m = self.message
m._finish_init(client, self._entities, None)
self.__dict__['_init'] = True # No new attributes can be set
def __getattr__(self, item):
if item in self.__dict__:
return self.__dict__[item]
else:
return getattr(self.message, item)
def __setattr__(self, name, value):
if not self.__dict__['_init'] or name in self.__dict__:
self.__dict__[name] = value
else:
setattr(self.message, name, value)

View File

@ -1,41 +0,0 @@
from .common import EventBuilder
from .. import utils
class Raw(EventBuilder):
"""
Raw events are not actual events. Instead, they are the raw
:tl:`Update` object that Telegram sends. You normally shouldn't
need these.
Args:
types (`list` | `tuple` | `type`, optional):
The type or types that the :tl:`Update` instance must be.
Equivalent to ``if not isinstance(update, types): return``.
"""
def __init__(self, types=None, *, func=None):
super().__init__(func=func)
if not types:
self.types = None
elif not utils.is_list_like(types):
if not isinstance(types, type):
raise TypeError('Invalid input type given %s', types)
self.types = types
else:
if not all(isinstance(x, type) for x in types):
raise TypeError('Invalid input types given %s', types)
self.types = tuple(types)
async def resolve(self, client):
self.resolved = True
@classmethod
def build(cls, update):
return update
def filter(self, event):
if ((not self.types or isinstance(event, self.types))
and (not self.func or self.func(event))):
return event

View File

@ -1,211 +0,0 @@
import datetime
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from ..tl import types
from ..tl.custom.sendergetter import SenderGetter
@name_inner_event
class UserUpdate(EventBuilder):
"""
Occurs whenever a user goes online, starts typing, etc.
"""
@classmethod
def build(cls, update):
if isinstance(update, types.UpdateUserStatus):
event = cls.Event(update.user_id,
status=update.status)
elif isinstance(update, types.UpdateChatUserTyping):
# Unfortunately, we can't know whether `chat_id`'s type
event = cls.Event(update.user_id,
chat_id=update.chat_id,
typing=update.action)
elif isinstance(update, types.UpdateUserTyping):
event = cls.Event(update.user_id,
typing=update.action)
else:
return
event._entities = update._entities
return event
class Event(EventCommon, SenderGetter):
"""
Represents the event of a user update
such as gone online, started typing, etc.
Members:
online (`bool`, optional):
``True`` if the user is currently online, ``False`` otherwise.
Might be ``None`` if this information is not present.
last_seen (`datetime`, optional):
Exact date when the user was last seen if known.
until (`datetime`, optional):
Until when will the user remain online.
within_months (`bool`):
``True`` if the user was seen within 30 days.
within_weeks (`bool`):
``True`` if the user was seen within 7 days.
recently (`bool`):
``True`` if the user was seen within a day.
action (:tl:`SendMessageAction`, optional):
The "typing" action if any the user is performing if any.
cancel (`bool`):
``True`` if the action was cancelling other actions.
typing (`bool`):
``True`` if the action is typing a message.
recording (`bool`):
``True`` if the action is recording something.
uploading (`bool`):
``True`` if the action is uploading something.
playing (`bool`):
``True`` if the action is playing a game.
audio (`bool`):
``True`` if what's being recorded/uploaded is an audio.
round (`bool`):
``True`` if what's being recorded/uploaded is a round video.
video (`bool`):
``True`` if what's being recorded/uploaded is an video.
document (`bool`):
``True`` if what's being uploaded is document.
geo (`bool`):
``True`` if what's being uploaded is a geo.
photo (`bool`):
``True`` if what's being uploaded is a photo.
contact (`bool`):
``True`` if what's being uploaded (selected) is a contact.
"""
def __init__(self, user_id, *, status=None, chat_id=None, typing=None):
if chat_id is None:
super().__init__(types.PeerUser(user_id))
else:
# Temporarily set the chat_peer to the ID until ._set_client.
# We need the client to actually figure out its type.
super().__init__(chat_id)
SenderGetter.__init__(self, user_id)
self.online = None if status is None else \
isinstance(status, types.UserStatusOnline)
self.last_seen = status.was_online if \
isinstance(status, types.UserStatusOffline) else None
self.until = status.expires if \
isinstance(status, types.UserStatusOnline) else None
if self.last_seen:
now = datetime.datetime.now(tz=datetime.timezone.utc)
diff = now - self.last_seen
if diff < datetime.timedelta(days=30):
self.within_months = True
if diff < datetime.timedelta(days=7):
self.within_weeks = True
if diff < datetime.timedelta(days=1):
self.recently = True
else:
self.within_months = self.within_weeks = self.recently = False
if isinstance(status, (types.UserStatusOnline,
types.UserStatusRecently)):
self.within_months = self.within_weeks = True
self.recently = True
elif isinstance(status, types.UserStatusLastWeek):
self.within_months = self.within_weeks = True
elif isinstance(status, types.UserStatusLastMonth):
self.within_months = True
self.action = typing
if typing:
self.cancel = self.typing = self.recording = self.uploading = \
self.playing = False
self.audio = self.round = self.video = self.document = \
self.geo = self.photo = self.contact = False
if isinstance(typing, types.SendMessageCancelAction):
self.cancel = True
elif isinstance(typing, types.SendMessageTypingAction):
self.typing = True
elif isinstance(typing, types.SendMessageGamePlayAction):
self.playing = True
elif isinstance(typing, types.SendMessageGeoLocationAction):
self.geo = True
elif isinstance(typing, types.SendMessageRecordAudioAction):
self.recording = self.audio = True
elif isinstance(typing, types.SendMessageRecordRoundAction):
self.recording = self.round = True
elif isinstance(typing, types.SendMessageRecordVideoAction):
self.recording = self.video = True
elif isinstance(typing, types.SendMessageChooseContactAction):
self.uploading = self.contact = True
elif isinstance(typing, types.SendMessageUploadAudioAction):
self.uploading = self.audio = True
elif isinstance(typing, types.SendMessageUploadDocumentAction):
self.uploading = self.document = True
elif isinstance(typing, types.SendMessageUploadPhotoAction):
self.uploading = self.photo = True
elif isinstance(typing, types.SendMessageUploadRoundAction):
self.uploading = self.round = True
elif isinstance(typing, types.SendMessageUploadVideoAction):
self.uploading = self.video = True
def _set_client(self, client):
if isinstance(self._chat_peer, int):
try:
chat = client._entity_cache[self._chat_peer]
if isinstance(chat, types.InputPeerChat):
self._chat_peer = types.PeerChat(self._chat_peer)
elif isinstance(chat, types.InputPeerChannel):
self._chat_peer = types.PeerChannel(self._chat_peer)
else:
# Should not happen
self._chat_peer = types.PeerUser(self._chat_peer)
except KeyError:
# Hope for the best. We don't know where this event
# occurred but it was most likely in a channel.
self._chat_peer = types.PeerChannel(self._chat_peer)
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache)
@property
def user(self):
"""Alias for `sender <telethon.tl.custom.sendergetter.SenderGetter.sender>`."""
return self.sender
async def get_user(self):
"""Alias for `get_sender <telethon.tl.custom.sendergetter.SenderGetter.get_sender>`."""
return await self.get_sender()
@property
def input_user(self):
"""Alias for `input_sender <telethon.tl.custom.sendergetter.SenderGetter.input_sender>`."""
return self.input_sender
async def get_input_user(self):
"""Alias for `get_input_sender <telethon.tl.custom.sendergetter.SenderGetter.get_input_sender>`."""
return await self.get_input_sender()
@property
def user_id(self):
"""Alias for `sender_id <telethon.tl.custom.sendergetter.SenderGetter.sender_id>`."""
return self.sender_id

View File

@ -1,6 +0,0 @@
"""
Several extensions Python is missing, such as a proper class to handle a TCP
communication with support for cancelling the operation, and an utility class
to read arbitrary binary data in a more comfortable way, with int/strings/etc.
"""
from .binaryreader import BinaryReader

View File

@ -1,195 +0,0 @@
"""
This module contains the BinaryReader utility class.
"""
import os
from datetime import datetime, timezone
from io import BufferedReader, BytesIO
from struct import unpack
from ..errors import TypeNotFoundError
from ..tl.alltlobjects import tlobjects
from ..tl.core import core_objects
class BinaryReader:
"""
Small utility class to read binary data.
Also creates a "Memory Stream" if necessary
"""
def __init__(self, data=None, stream=None):
if data:
self.stream = BytesIO(data)
elif stream:
self.stream = stream
else:
raise ValueError('Either bytes or a stream must be provided')
self.reader = BufferedReader(self.stream)
self._last = None # Should come in handy to spot -404 errors
# region Reading
# "All numbers are written as little endian."
# https://core.telegram.org/mtproto
def read_byte(self):
"""Reads a single byte value."""
return self.read(1)[0]
def read_int(self, signed=True):
"""Reads an integer (4 bytes) value."""
return int.from_bytes(self.read(4), byteorder='little', signed=signed)
def read_long(self, signed=True):
"""Reads a long integer (8 bytes) value."""
return int.from_bytes(self.read(8), byteorder='little', signed=signed)
def read_float(self):
"""Reads a real floating point (4 bytes) value."""
return unpack('<f', self.read(4))[0]
def read_double(self):
"""Reads a real floating point (8 bytes) value."""
return unpack('<d', self.read(8))[0]
def read_large_int(self, bits, signed=True):
"""Reads a n-bits long integer value."""
return int.from_bytes(
self.read(bits // 8), byteorder='little', signed=signed)
def read(self, length=None):
"""Read the given amount of bytes."""
if length is None:
return self.reader.read()
result = self.reader.read(length)
if len(result) != length:
raise BufferError(
'No more data left to read (need {}, got {}: {}); last read {}'
.format(length, len(result), repr(result), repr(self._last))
)
self._last = result
return result
def get_bytes(self):
"""Gets the byte array representing the current buffer as a whole."""
return self.stream.getvalue()
# endregion
# region Telegram custom reading
def tgread_bytes(self):
"""
Reads a Telegram-encoded byte array, without the need of
specifying its length.
"""
first_byte = self.read_byte()
if first_byte == 254:
length = self.read_byte() | (self.read_byte() << 8) | (
self.read_byte() << 16)
padding = length % 4
else:
length = first_byte
padding = (length + 1) % 4
data = self.read(length)
if padding > 0:
padding = 4 - padding
self.read(padding)
return data
def tgread_string(self):
"""Reads a Telegram-encoded string."""
return str(self.tgread_bytes(), encoding='utf-8', errors='replace')
def tgread_bool(self):
"""Reads a Telegram boolean value."""
value = self.read_int(signed=False)
if value == 0x997275b5: # boolTrue
return True
elif value == 0xbc799737: # boolFalse
return False
else:
raise RuntimeError('Invalid boolean code {}'.format(hex(value)))
def tgread_date(self):
"""Reads and converts Unix time (used by Telegram)
into a Python datetime object.
"""
value = self.read_int()
if value == 0:
return None
else:
return datetime.fromtimestamp(value, tz=timezone.utc)
def tgread_object(self):
"""Reads a Telegram object."""
constructor_id = self.read_int(signed=False)
clazz = tlobjects.get(constructor_id, None)
if clazz is None:
# The class was None, but there's still a
# chance of it being a manually parsed value like bool!
value = constructor_id
if value == 0x997275b5: # boolTrue
return True
elif value == 0xbc799737: # boolFalse
return False
elif value == 0x1cb5c415: # Vector
return [self.tgread_object() for _ in range(self.read_int())]
clazz = core_objects.get(constructor_id, None)
if clazz is None:
# If there was still no luck, give up
self.seek(-4) # Go back
pos = self.tell_position()
error = TypeNotFoundError(constructor_id, self.read())
self.set_position(pos)
raise error
return clazz.from_reader(self)
def tgread_vector(self):
"""Reads a vector (a list) of Telegram objects."""
if 0x1cb5c415 != self.read_int(signed=False):
raise RuntimeError('Invalid constructor code, vector was expected')
count = self.read_int()
return [self.tgread_object() for _ in range(count)]
# endregion
def close(self):
"""Closes the reader, freeing the BytesIO stream."""
self.reader.close()
# region Position related
def tell_position(self):
"""Tells the current position on the stream."""
return self.reader.tell()
def set_position(self, position):
"""Sets the current position on the stream."""
self.reader.seek(position)
def seek(self, offset):
"""
Seeks the stream position given an offset from the current position.
The offset may be negative.
"""
self.reader.seek(offset, os.SEEK_CUR)
# endregion
# region with block
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
# endregion

View File

@ -1,215 +0,0 @@
"""
Simple HTML -> Telegram entity parser.
"""
import struct
from collections import deque
from html import escape, unescape
from html.parser import HTMLParser
from typing import Iterable, Optional, Tuple, List
from .. import helpers
from ..tl.types import (
MessageEntityBold, MessageEntityItalic, MessageEntityCode,
MessageEntityPre, MessageEntityEmail, MessageEntityUrl,
MessageEntityTextUrl, MessageEntityMentionName,
MessageEntityUnderline, MessageEntityStrike, MessageEntityBlockquote,
TypeMessageEntity
)
# Helpers from markdown.py
def _add_surrogate(text):
return ''.join(
''.join(chr(y) for y in struct.unpack('<HH', x.encode('utf-16le')))
if (0x10000 <= ord(x) <= 0x10FFFF) else x for x in text
)
def _del_surrogate(text):
return text.encode('utf-16', 'surrogatepass').decode('utf-16')
class HTMLToTelegramParser(HTMLParser):
def __init__(self):
super().__init__()
self.text = ''
self.entities = []
self._building_entities = {}
self._open_tags = deque()
self._open_tags_meta = deque()
def handle_starttag(self, tag, attrs):
self._open_tags.appendleft(tag)
self._open_tags_meta.appendleft(None)
attrs = dict(attrs)
EntityType = None
args = {}
if tag == 'strong' or tag == 'b':
EntityType = MessageEntityBold
elif tag == 'em' or tag == 'i':
EntityType = MessageEntityItalic
elif tag == 'u':
EntityType = MessageEntityUnderline
elif tag == 'del' or tag == 's':
EntityType = MessageEntityStrike
elif tag == 'blockquote':
EntityType = MessageEntityBlockquote
elif tag == 'code':
try:
# If we're in the middle of a <pre> tag, this <code> tag is
# probably intended for syntax highlighting.
#
# Syntax highlighting is set with
# <code class='language-...'>codeblock</code>
# inside <pre> tags
pre = self._building_entities['pre']
try:
pre.language = attrs['class'][len('language-'):]
except KeyError:
pass
except KeyError:
EntityType = MessageEntityCode
elif tag == 'pre':
EntityType = MessageEntityPre
args['language'] = ''
elif tag == 'a':
try:
url = attrs['href']
except KeyError:
return
if url.startswith('mailto:'):
url = url[len('mailto:'):]
EntityType = MessageEntityEmail
else:
if self.get_starttag_text() == url:
EntityType = MessageEntityUrl
else:
EntityType = MessageEntityTextUrl
args['url'] = url
url = None
self._open_tags_meta.popleft()
self._open_tags_meta.appendleft(url)
if EntityType and tag not in self._building_entities:
self._building_entities[tag] = EntityType(
offset=len(self.text),
# The length will be determined when closing the tag.
length=0,
**args)
def handle_data(self, text):
text = unescape(text)
previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else ''
if previous_tag == 'a':
url = self._open_tags_meta[0]
if url:
text = url
for tag, entity in self._building_entities.items():
entity.length += len(text)
self.text += text
def handle_endtag(self, tag):
try:
self._open_tags.popleft()
self._open_tags_meta.popleft()
except IndexError:
pass
entity = self._building_entities.pop(tag, None)
if entity:
self.entities.append(entity)
def parse(html: str) -> Tuple[str, List[TypeMessageEntity]]:
"""
Parses the given HTML message and returns its stripped representation
plus a list of the MessageEntity's that were found.
:param message: the message with HTML to be parsed.
:return: a tuple consisting of (clean message, [message entities]).
"""
if not html:
return html, []
parser = HTMLToTelegramParser()
parser.feed(_add_surrogate(html))
text = helpers.strip_text(parser.text, parser.entities)
return _del_surrogate(text), parser.entities
def unparse(text: str, entities: Iterable[TypeMessageEntity], _offset: int = 0,
_length: Optional[int] = None) -> str:
"""
Performs the reverse operation to .parse(), effectively returning HTML
given a normal text and its MessageEntity's.
:param text: the text to be reconverted into HTML.
:param entities: the MessageEntity's applied to the text.
:return: a HTML representation of the combination of both inputs.
"""
if not text:
return text
elif not entities:
return escape(text)
text = _add_surrogate(text)
if _length is None:
_length = len(text)
html = []
last_offset = 0
for i, entity in enumerate(entities):
if entity.offset > _offset + _length:
break
relative_offset = entity.offset - _offset
if relative_offset > last_offset:
html.append(escape(text[last_offset:relative_offset]))
elif relative_offset < last_offset:
continue
skip_entity = False
entity_text = unparse(text=text[relative_offset:relative_offset + entity.length],
entities=entities[i + 1:],
_offset=entity.offset, _length=entity.length)
entity_type = type(entity)
if entity_type == MessageEntityBold:
html.append('<strong>{}</strong>'.format(entity_text))
elif entity_type == MessageEntityItalic:
html.append('<em>{}</em>'.format(entity_text))
elif entity_type == MessageEntityCode:
html.append('<code>{}</code>'.format(entity_text))
elif entity_type == MessageEntityUnderline:
html.append('<u>{}</u>'.format(entity_text))
elif entity_type == MessageEntityStrike:
html.append('<del>{}</del>'.format(entity_text))
elif entity_type == MessageEntityBlockquote:
html.append('<blockquote>{}</blockquote>'.format(entity_text))
elif entity_type == MessageEntityPre:
if entity.language:
html.append(
"<pre>\n"
" <code class='language-{}'>\n"
" {}\n"
" </code>\n"
"</pre>".format(entity.language, entity_text))
else:
html.append('<pre><code>{}</code></pre>'
.format(entity_text))
elif entity_type == MessageEntityEmail:
html.append('<a href="mailto:{0}">{0}</a>'.format(entity_text))
elif entity_type == MessageEntityUrl:
html.append('<a href="{0}">{0}</a>'.format(entity_text))
elif entity_type == MessageEntityTextUrl:
html.append('<a href="{}">{}</a>'
.format(escape(entity.url), entity_text))
elif entity_type == MessageEntityMentionName:
html.append('<a href="tg://user?id={}">{}</a>'
.format(entity.user_id, entity_text))
else:
skip_entity = True
last_offset = relative_offset + (0 if skip_entity else entity.length)
html.append(escape(text[last_offset:]))
return _del_surrogate(''.join(html))

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