Continue documentation and matching documented behaviour

This commit is contained in:
Lonami Exo
2023-11-02 00:45:11 +01:00
parent 6e88264b28
commit 2def0a169c
21 changed files with 450 additions and 95 deletions
+3 -3
View File
@@ -29,10 +29,10 @@ Once you have a working Python 3 installation, you can install or upgrade the ``
.. code-block:: shell
python -m pip install --upgrade telethon
python -m pip install --upgrade "telethon~=2.0"
Be sure to use lock-files if your project!
The above is just a quick way to get started and install Telethon globally.
The above is just a quick way to get started and install a `v2-compatible <https://peps.python.org/pep-0440/#compatible-release>`_ Telethon globally.
Installing development versions
@@ -47,7 +47,7 @@ If you want the *latest* unreleased changes, you can run the following command i
.. note::
The development version may have bugs and is not recommended for production use.
However, when you are `reporting a library bug <https://github.com/LonamiWebs/Telethon/issues/>`,
However, when you are `reporting a library bug <https://github.com/LonamiWebs/Telethon/issues/>`_,
you must reproduce the issue in this version before reporting the problem.
+8 -1
View File
@@ -50,6 +50,8 @@ If the issue persists, you may try contacting them, using a proxy or using a VPN
Be aware that some phone numbers are not eligible to register applications with.
.. _interactive login:
Interactive login
-----------------
@@ -131,6 +133,11 @@ If you want to automatically login as a bot when needed, you can do so without a
Manual login
------------
.. tip::
You can safely skip to :doc:`next-steps` if you've already completed the :ref:`interactive login`.
This section is only of interest if you want more control over how to manually login.
We've talked about the second and third parameters of the :class:`Client` constructor, but not the first:
.. code-block:: python
@@ -143,7 +150,7 @@ The session path can contain directory separators and live anywhere in the file
Telethon will automatically append the ``.session`` extension if you don't provide any.
Briefly, the session contains some of the information needed to connect to Telegram.
This includes the datacenter belonging to the account logged-in, and the authorization key used for encryption, among other things.
This includes the data center belonging to the account logged-in, and the authorization key used for encryption, among other things.
.. important::
+3 -3
View File
@@ -72,7 +72,7 @@ There is no HTTP connection, no "polling", and no "web hooks".
We can compare the two visually:
.. graphviz::
:caption: Communication between a Client and the Bot API
:caption: Communication between a Client and the HTTP Bot API
digraph botapi {
rankdir=LR;
@@ -86,7 +86,7 @@ We can compare the two visually:
}
.. graphviz::
:caption: Communication between a Client and the MTProto API
:caption: Communication between a Client and Telegram's API via MTProto
digraph botapi {
rankdir=LR;
@@ -119,7 +119,7 @@ If the above points convinced you to switch to Telethon, the following short gui
It doesn't matter if you wrote your bot with `requests <https://pypi.org/project/requests/>`_
and you were making API requests manually, or if you used a wrapper library like
`python-telegram-bot <https://python-telegram-bot.readthedocs.io>`_
or `pyTelegramBotAPI <https://pytba.readthedocs.io/en/latest/index.html>`.
or `pyTelegramBotAPI <https://pytba.readthedocs.io/en/latest/index.html>`_.
You will surely be pleased with Telethon!
If you were using an asynchronous library like `aiohttp <https://docs.aiohttp.org/en/stable/>`_
+14 -3
View File
@@ -39,7 +39,7 @@ Telegram Chat
The Telegram API is very confusing when it comes to the word "chat".
You only need to know about this if you plan to use the :term:`Raw API`.
In the schema definitions, there are two boxed types, :tl:`User` and :tl:`Chat`.
In the :term:`TL` schema definitions, there are two boxed types, :tl:`User` and :tl:`Chat`.
A boxed :tl:`User` can only be the bare :tl:`user`, but the boxed :tl:`Chat` can be either a bare :tl:`chat` or a bare :tl:`channel`.
A bare :tl:`chat` always refers to small groups.
@@ -48,7 +48,7 @@ A bare :tl:`channel` can have either the ``broadcast`` or the ``megagroup`` flag
A bare :tl:`channel` with the ``broadcast`` flag set to :data:`True` is known as a broadcast channel.
A bare :tl:`channel` with the ``megagroup`` flag set to :data:`True` is known as a supergroup.
A bare :tl:`chat` with has less features than a bare :tl:`channel` ``megagroup``.
A bare :tl:`chat` has less features available than a bare :tl:`channel` ``megagroup``.
Official clients are very good at hiding this difference.
They will implicitly convert bare :tl:`chat` to bare :tl:`channel` ``megagroup`` when doing certain operations.
Doing things like setting a username is actually a two-step process (migration followed by updating the username).
@@ -70,11 +70,21 @@ The Bot API follows a certain convention when it comes to identifiers:
* User IDs are positive.
* Chat IDs are negative.
* Channel IDs are prefixed with ``-100``.
* Channel IDs are *also* negative, but are prefixed by ``-100``.
Telethon encourages the use of :class:`~types.PackedChat` instead of naked identifiers.
As a reminder, negative identifiers are not supported in Telethon's chat-like parameters.
If you got an Bot API-style ID from somewhere else, you will need to explicitly say what type it is:
.. code-block:: python
# If -1001234 is your ID...
from telethon.types import PackedChat, PackedType
chat = PackedChat(PackedType.BROADCAST, 1234, None)
# ...you need to explicitly create a PackedChat with id=1234 and set the corresponding type (a channel).
# The access hash (see below) will be ``None``, which may or may not work.
Encountering chats
------------------
@@ -88,6 +98,7 @@ If you:
* …know the username of the user, group, or channel, you can :meth:`~Client.resolve_username`.
* …are a bot responding to users, you will be able to access the :attr:`types.Message.sender`.
Chats access hash
-----------------
+125
View File
@@ -0,0 +1,125 @@
Data centers
============
.. currentmodule:: telethon
Telegram has multiple servers, known as *data centers* or MTProto servers, all over the globe.
This makes it possible to have reasonably low latency when sending messages.
When an account is created, Telegram chooses the most appropriated data center for you.
This means you *cannot* change what your "home data center" is.
However, `Telegram may change it after prolongued use from other locations <https://core.telegram.org/api/datacenter>`_.
Connecting behind a proxy
-------------------------
You can change the way Telethon opens a connection to Telegram's data center by setting a different :class:`~telethon._impl.mtsender.sender.Connector`.
A connector is a function returning an asynchronous reader-writer pair.
The default connector is :func:`asyncio.open_connection`, defined as:
.. code-block:: python
def default_connector(ip: str, port: int):
return asyncio.open_connection(ip, port)
While proxies are not directly supported in Telethon, you can change the connector to use a proxy.
Any proxy library that supports :mod:`asyncio`, such as `python-socks[asyncio] <https://pypi.org/project/python-socks/>`_, can be used:
.. code-block:: python
import asyncio
from functools import partial
from python_socks.async_.asyncio import Proxy
from telethon import Client
async def my_proxy_connector(ip, port, *, proxy_url):
# Refer to python-socks for an up-to-date way to define and use proxies.
# This is just an example of a custom connector.
proxy = Proxy.from_url(proxy_url)
sock = await proxy.connect(dest_host='example.com', dest_port=443)
return await asyncio.open_connection(
host=ip,
port=port,
sock=sock,
ssl=ssl.create_default_context(),
server_hostname='example.com',
)
client = Client(..., connector=partial(
my_proxy_connector,
proxy_url='socks5://user:[email protected]:1080'
))
.. important::
Proxies can be used with Telethon, but they are not directly supported.
Any connection errors you encounter while using a proxy are therefore very unlikely to be errors in Telethon.
Connection errors when using custom connectors will *not* be considered bugs in the Telethon.
.. note::
Some proxies only support HTTP traffic.
Telethon by default does not transmit HTTP-encoded packets.
This means some HTTP-only proxies may not work.
Test servers
------------
While you cannot change the production data center assigned to your account, you can tell Telethon to connect to a different server.
This is most useful to connect to the official Telegram test servers or `even your own <https://github.com/DavideGalilei/piltover>`_.
You need to import and define the :class:`session.DataCenter` to connect to when creating the :class:`Client`:
.. code-block:: python
from telethon import Client
from telethon.session import DataCenter
client = Client(..., datacenter=DataCenter(id=2, ipv4_addr='149.154.167.40:443'))
This will override the value coming from the :class:`~session.Session`.
You can get the test address for your account from `My Telegram <https://my.telegram.org>`_.
.. note::
Make sure the :doc:`sessions` you use for this client had not been created for the production servers before.
The library will attempt to use the existing authorization key saved based on the data center identifier.
This will most likely fail if you mix production and test servers.
There are public phone numbers anyone can use, with the following format:
.. code-block::
:caption: 99966XYYYY test phone number, X being the datacenter identifier and YYYY random digits
99966 X YYYY
\___/ \_/ \__/
| | `- random number
| `- datacenter identifier
`- fixed digits
For example, the test phone number 1234 for the datacenter 2 would be 9996621234.
The confirmation code to complete the login is the datacenter identifier repeated five times, in this case, 22222.
Therefore, it is possible to automate the login procedure, assuming the account exists and there is no 2-factor authentication:
.. code-block:: python
from random import randrange
from telethon import Client
from telethon.session import DataCenter
datacenter = DataCenter(id=2, ipv4_addr='149.154.167.40:443')
phone = f'{randrange(1, 9999):04}'
login_code = str(datacenter.id) * 5
client = Client(..., datacenter=datacenter)
async with client:
if not await client.is_authorized():
login_token = await client.request_login_code(phone_or_token)
await client.sign_in(login_token, login_code)
+8 -8
View File
@@ -9,7 +9,8 @@ In Telethon, a :term:`RPC error` corresponds to the :class:`RpcError` class.
Telethon will only ever raise :class:`RpcError` when the result to a :term:`RPC` is an error.
If the error is raised, you know it comes from Telegram.
Consequently, when using :term:`Raw API`, if a :class:`RpcError` occurs, it is never a bug in the library.
Consequently, when using :term:`Raw API` directly, if a :class:`RpcError` occurs, it is *extremely unlikely* to be a bug in the library.
When :class:`RpcError`\ s are raised using the :term:`Raw API`, Telegram is the one that decided an error should occur.
:term:`RPC error` consist of an integer :attr:`~RpcError.code` and a string :attr:`~RpcError.name`.
The :attr:`RpcError.code` is roughly the same as `HTTP status codes <https://developer.mozilla.org/en-US/docs/Web/HTTP/Status>`_.
@@ -25,16 +26,15 @@ It occurs when you have attempted to use a request too many times during a certa
.. code-block:: python
import asyncio
from telethon import RpcError
from telethon import errors
try:
await client.send_message('me', 'Spam')
except RpcError as e:
# If we get a flood error, sleep. Else, propagate the error.
if e.name == 'FLOOD_WAIT':
await asyncio.sleep(e.value)
else:
raise
except errors.FloodWait as e:
# A flood error; sleep.
await asyncio.sleep(e.value)
Note that the library can automatically handle and retry on ``FLOOD_WAIT`` for you.
Refer to the ``flood_sleep_threshold`` of the :class:`Client` to learn how.
Refer to the documentation of the :data:`telethon.errors` pseudo-module for more details.
+1 -1
View File
@@ -10,7 +10,7 @@ Telethon concedes to this fact and implements only commonly-used features to kee
Access to the entirity of Telegram's API via Telethon's :term:`Raw API` is a necessary evil.
The ``telethon._tl`` module has a leading underscore to signal that it is private.
It is not covered by the semver guarantees of the library, but you may need to use it regardless.
It is not covered by the `semver <https://semver.org/>`_ guarantees of the library, but you may need to use it regardless.
If the :class:`Client` doesn't offer a method for what you need, using the :term:`Raw API` is inevitable.
+37
View File
@@ -15,6 +15,43 @@ Messages
Messages are at the heart of a messaging platform.
In Telethon, you will be using the :class:`~types.Message` class to interact with them.
Fetching messages
-----------------
The most common way to actively fetch messages using the :meth:`Client.get_messages` method:
.. code-block:: python
# Get the last message in a chat (by setting the limit to 1).
last_message = (await client.get_messages(chat, 1))[0]
# Iterate over all messages in a chat, starting from the oldest message (by using reversed).
async for message in reversed(client.get_messages(chat)):
print(message.sender.name, message.text_html)
You can also perform a fuzzy text search with the :meth:`Client.search_messages` method.
The search will be performed server-side by Telegram, so the rules for how it works are also fuzzy.
If you want to search for messages in all the chats you're part of, you can use :meth:`Client.search_all_messages`.
Lastly, :meth:`Client.send_message` *also* returns the :class:`~types.Message` that you just sent.
The most common way to passively listen to incoming messages is using the :class:`~events.NewMessage` event:
.. code-block:: python
from telethon import events
@client.on(events.NewMessage)
async def first(event):
print(event.chat.name, ':', event.text)
.. seealso::
The :doc:`updates` concept for an in-depth explanation on using events.
.. _formatting:
Formatting messages
+4 -1
View File
@@ -4,7 +4,7 @@ Sessions
.. currentmodule:: telethon
In Telethon, the word :term:`session` is used to refer to the set of data needed to connect to Telegram.
This includes the server address of your home datacenter, as well as the authorization key bound to an account.
This includes the server address of your home data center, as well as the authorization key bound to an account.
When you first connect to Telegram, an authorization key is generated to encrypt all communication.
After login, Telegram remembers this authorization key as logged-in, so you don't need to login again.
@@ -48,6 +48,9 @@ Telethon comes with two built-in storages:
It's useful when you don't have file-system access.
If you would like to store the session state in a different way, you can subclass :class:`session.Storage`.
You may also find `custom third-party session storages in Telethon's wiki <https://github.com/LonamiWebs/Telethon/wiki/Session-Storages>`_.
Be careful with any third-party code you install, as they could steal the login credentials.
Only use session storages you trust, and pin the specific versions you have audited.
Some Python installations do not have the ``sqlite3`` module.
In this case, attempting to use the default :class:`~session.SqliteSession` will fail.
+80 -2
View File
@@ -23,6 +23,55 @@ Telethon abstracts away Telegram updates with :mod:`~telethon.events`.
With the above, you will see all warnings and errors and when they happened.
Listening to updates
--------------------
You can define and register your own functions to be called when certain :mod:`telethon.events` occur.
The most common way is using the :meth:`Client.on` decorator to register your callback functions, often referred to as *handlers*:
.. code-block:: python
from telethon import Client, events
from telethon.events import filters
bot = Client(...)
@bot.on(events.NewMessage, filters.Command('/start'))
async def handler(event: events.NewMessage):
await event.respond('Beep boop!')
The first parameter is the :class:`type` of one of the :mod:`telethon.events`, not an instance, so make sure you don't write parenthesis after it.
The second parameter is optional.
If provided, it must be a callable function that returns :data:`True` if the handler should run.
Built-in filter functions are available in the :mod:`~telethon.events.filters` module.
In this example, :class:`~events.filters.Command` means the handler will be called when the user sends */start* to the bot.
When your ``handler`` function is called, it will receive a single parameter, the event.
The event type is the same as the one you defined in the decorator when registering your handler.
You don't need to explicitly set the type hint, but you can do so if you want your IDE to assist in autocompletion.
If you cannot use decorators, you can use the :meth:`Client.add_event_handler` method instead.
The above code is equivalent to the following:
.. code-block:: python
from telethon import Client, events
from telethon.events import filters
async def handler(event: events.NewMessage):
await event.respond('Beep boop!')
bot = Client(...)
bot.add_event_handler(handler, events.NewMessage, filters.Command('/start'))
Note how the above lets you defined the :class:`Client` instance *after* your handlers.
In other words, you can define your handlers without the :class:`Client` instance.
This may make it easier to place them in a separate file.
Filtering events
----------------
@@ -51,6 +100,12 @@ If you need state, you can use a class with a ``__call__`` method defined:
.. code-block:: python
# Anonymous filter which only handles messages with ID = 1000
client.add_event_handler(handler, events.NewMessage, lambda e: e.id == 1000)
# this parameter is the filter ^--------------------^
# ...
def only_odd_messages(event):
"A filter that only handles messages when their ID is divisible by 2"
return event.id % 2 == 0
@@ -75,6 +130,16 @@ You can use :func:`isinstance` if your filter can only deal with certain types o
If you need to perform asynchronous operations, you can't use a filter.
Instead, manually check for those conditions inside your handler.
The filters work all the same when using :meth:`Client.on`.
This makes it very convenient to write custom filters using the :keyword:`lambda` syntax:
.. code-block:: python
@client.on(events.NewMessage, lambda e: e.id == 1000)
async def handler(event):
...
Setting priority on handlers
----------------------------
@@ -100,13 +165,26 @@ This is often the desired behaviour if you're using filters.
If you have more complicated filters executed *inside* the handler,
Telethon believes your handler completed and will stop calling the rest.
If that's the case, you can instruct Telethon to check all your handlers:
If that's the case, you can :keyword:`return` :class:`events.Continue`:
.. code-block:: python
@client.on(events.NewMessage)
async def first(event):
print('This is always called on new messages!')
return events.Continue
@client.on(events.NewMessage)
async def second(event):
print('Now this one runs as well!')
Alternatively, if this is *always* the behaviour you want, you can configure it in the :class:`Client`:
.. code-block:: python
client = Client(..., check_all_handlers=True)
# ^^^^^^^^^^^^^^^^^^^^^^^
# Now the code above will call both handlers
# Now the code above will call both handlers, even without returning events.Continue
If you need a more complicated setup, consider sorting all your handlers beforehand.
Then, use :meth:`Client.add_event_handler` on all of them to ensure the correct order.
+1
View File
@@ -91,6 +91,7 @@ A more in-depth explanation of some of the concepts and words used in Telethon.
concepts/errors
concepts/botapi-vs-mtproto
concepts/full-api
concepts/datacenters
concepts/glossary
+1 -1
View File
@@ -7,6 +7,6 @@ The :class:`Client` class is the "entry point" of the library.
Most client methods have an alias in the respective types.
For example, :meth:`Client.forward_messages` can also be invoked from :meth:`types.Message.forward`.
With a few exceptions, "client.verb_object" methods also exist as "object.verb".
With a few exceptions, *client.verb_object* methods also exist as *object.verb*.
.. autoclass:: Client