mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-02-16 19:41:07 +03:00
Continue documentation and matching documented behaviour
This commit is contained in:
parent
6e88264b28
commit
2def0a169c
|
@ -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.
|
||||
|
||||
|
||||
|
|
|
@ -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::
|
||||
|
||||
|
|
|
@ -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/>`_
|
||||
|
|
|
@ -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
client/doc/concepts/datacenters.rst
Normal file
125
client/doc/concepts/datacenters.rst
Normal 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:password@127.0.0.1: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)
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
||||
|
|
|
@ -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,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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -152,6 +152,8 @@ class Client:
|
|||
:param catch_up:
|
||||
Whether to "catch up" on updates that occured while the client was not connected.
|
||||
|
||||
If :data:`True`, all updates that occured while the client was offline will trigger your :doc:`event handlers </concepts/updates>`.
|
||||
|
||||
:param check_all_handlers:
|
||||
Whether to always check all event handlers or stop early.
|
||||
|
||||
|
@ -159,21 +161,24 @@ class Client:
|
|||
By default, the library stops checking handlers as soon as a filter returns :data:`True`.
|
||||
|
||||
By setting ``check_all_handlers=True``, the library will keep calling handlers after the first match.
|
||||
Use :class:`telethon.events.Continue` instead if you only want this behaviour sometimes.
|
||||
|
||||
:param flood_sleep_threshold:
|
||||
Maximum amount of time, in seconds, to automatically sleep before retrying a request.
|
||||
This sleeping occurs when ``FLOOD_WAIT`` :class:`~telethon.RpcError` is raised by Telegram.
|
||||
This sleeping occurs when ``FLOOD_WAIT`` (and similar) :class:`~telethon.RpcError`\ s are raised by Telegram.
|
||||
|
||||
:param logger:
|
||||
Logger for the client.
|
||||
Any dependency of the client will use :meth:`logging.Logger.getChild`.
|
||||
This effectively makes the parameter the root logger.
|
||||
|
||||
The default will get the logger for the package name from the root.
|
||||
The default will get the logger for the package name from the root (usually *telethon*).
|
||||
|
||||
:param update_queue_limit:
|
||||
Maximum amount of updates to keep in memory before dropping them.
|
||||
|
||||
A warning will be logged on a cooldown if this limit is reached.
|
||||
|
||||
:param device_model:
|
||||
Device model.
|
||||
|
||||
|
@ -184,19 +189,19 @@ class Client:
|
|||
Application version.
|
||||
|
||||
:param system_lang_code:
|
||||
ISO 639-1 language code of the system's language.
|
||||
`ISO 639-1 <https://www.iso.org/iso-639-language-codes.html>`_ language code of the system's language.
|
||||
|
||||
:param lang_code:
|
||||
ISO 639-1 language code of the application's language.
|
||||
`ISO 639-1 <https://www.iso.org/iso-639-language-codes.html>`_ language code of the application's language.
|
||||
|
||||
:param datacenter:
|
||||
Override the datacenter to connect to.
|
||||
Override the :doc:`data center </concepts/datacenters>` to connect to.
|
||||
Useful to connect to one of Telegram's test servers.
|
||||
|
||||
:param connector:
|
||||
Asynchronous function called to connect to a remote address.
|
||||
By default, this is :func:`asyncio.open_connection`.
|
||||
In order to use proxies, you can set a custom connector.
|
||||
In order to :doc:`use proxies </concepts/datacenters>`, you can set a custom connector.
|
||||
|
||||
See :class:`~telethon._impl.mtsender.sender.Connector` for more details.
|
||||
"""
|
||||
|
@ -330,7 +335,13 @@ class Client:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
await client.bot_sign_in('12345:abc67DEF89ghi')
|
||||
user = await client.bot_sign_in('12345:abc67DEF89ghi')
|
||||
print('Signed in to bot account:', user.name)
|
||||
|
||||
.. caution::
|
||||
|
||||
Be sure to check :meth:`is_authorized` before calling this function.
|
||||
Signing in often when you don't need to will lead to :doc:`/concepts/errors`.
|
||||
|
||||
.. seealso::
|
||||
|
||||
|
@ -364,6 +375,7 @@ class Client:
|
|||
assert isinstance(password_token, PasswordToken)
|
||||
|
||||
user = await client.check_password(password_token, '1-L0V3+T3l3th0n')
|
||||
print('Signed in to 2FA-protected account:', user.name)
|
||||
|
||||
.. seealso::
|
||||
|
||||
|
@ -390,9 +402,9 @@ class Client:
|
|||
|
||||
This lets you leave a group, unsubscribe from a channel, or delete a one-to-one private conversation.
|
||||
|
||||
Note that the group or channel will not be deleted.
|
||||
Note that the group or channel will not be deleted (other users will remain in it).
|
||||
|
||||
Note that bot accounts do not have dialogs, so this method will fail.
|
||||
Note that bot accounts do not have dialogs, so this method will fail when used in a bot account.
|
||||
|
||||
:param chat:
|
||||
The :term:`chat` representing the dialog to delete.
|
||||
|
@ -420,8 +432,8 @@ class Client:
|
|||
.. warning::
|
||||
|
||||
When deleting messages from private conversations or small groups,
|
||||
this parameter is ignored. This means the *message_ids* may delete
|
||||
messages in different chats.
|
||||
this parameter is currently ignored.
|
||||
This means the *message_ids* may delete messages in different chats.
|
||||
|
||||
:param message_ids:
|
||||
The list of message identifiers to delete.
|
||||
|
@ -437,11 +449,12 @@ class Client:
|
|||
.. code-block:: python
|
||||
|
||||
# Delete two messages from chat for yourself
|
||||
await client.delete_messages(
|
||||
delete_count = await client.delete_messages(
|
||||
chat,
|
||||
[187481, 187482],
|
||||
revoke=False,
|
||||
)
|
||||
print('Deleted', delete_count, 'message(s)')
|
||||
|
||||
.. seealso::
|
||||
|
||||
|
@ -477,19 +490,19 @@ class Client:
|
|||
Note that the extension is not automatically added to the path.
|
||||
You can get the file extension with :attr:`telethon.types.File.ext`.
|
||||
|
||||
.. warning::
|
||||
.. caution::
|
||||
|
||||
If the file already exists, it will be overwritten!
|
||||
If the file already exists, it will be overwritten.
|
||||
|
||||
.. rubric:: Example
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
if photo := message.photo:
|
||||
await client.download(photo, 'picture.jpg')
|
||||
await client.download(photo, f'picture{photo.ext}')
|
||||
|
||||
if video := message.video:
|
||||
with open('video.mp4, 'wb') as file:
|
||||
with open(f'video{video.ext}', 'wb') as file:
|
||||
await client.download(video, file)
|
||||
|
||||
.. seealso::
|
||||
|
@ -530,15 +543,21 @@ class Client:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
# Edit message to have text without formatting
|
||||
await client.edit_message(chat, msg_id, text='New text')
|
||||
# Set a draft with no formatting and print the date Telegram registered
|
||||
draft = await client.edit_draft(chat, 'New text')
|
||||
print('Set current draft on', draft.date)
|
||||
|
||||
# Remove the link preview without changing the text
|
||||
await client.edit_message(chat, msg_id, link_preview=False)
|
||||
# Set a draft using HTML formatting, with a reply, and enabling the link preview
|
||||
await client.edit_draft(
|
||||
chat,
|
||||
html='Draft with <em>reply</em> an URL https://example.com',
|
||||
reply_to=message_id,
|
||||
link_preview=True
|
||||
)
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`telethon.types.Message.edit`
|
||||
:meth:`telethon.types.Draft.edit`
|
||||
"""
|
||||
return await edit_draft(
|
||||
self,
|
||||
|
@ -622,11 +641,12 @@ class Client:
|
|||
.. code-block:: python
|
||||
|
||||
# Forward two messages from chat to the destination
|
||||
await client.forward_messages(
|
||||
messages = await client.forward_messages(
|
||||
destination,
|
||||
[187481, 187482],
|
||||
chat,
|
||||
)
|
||||
print('Forwarded', len(messages), 'message(s)')
|
||||
|
||||
.. seealso::
|
||||
|
||||
|
@ -704,6 +724,7 @@ class Client:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
# Clear all drafts
|
||||
async for draft in client.get_drafts():
|
||||
await draft.delete()
|
||||
"""
|
||||
|
@ -794,10 +815,12 @@ class Client:
|
|||
offset_date: Optional[datetime.datetime] = None,
|
||||
) -> AsyncList[Message]:
|
||||
"""
|
||||
Get the message history from a :term:`chat`.
|
||||
Get the message history from a :term:`chat`, from the newest message to the oldest.
|
||||
|
||||
The returned iterator can be :func:`reversed` to fetch from the first to the last instead.
|
||||
|
||||
:param chat:
|
||||
The :term:`chat` where the message to edit is.
|
||||
The :term:`chat` where the messages should be fetched from.
|
||||
|
||||
:param limit:
|
||||
How many messages to fetch at most.
|
||||
|
@ -818,12 +841,17 @@ class Client:
|
|||
|
||||
# Get the last message in a chat
|
||||
last_message = (await client.get_messages(chat, 1))[0]
|
||||
print(message.sender.name, last_message.text)
|
||||
|
||||
# Print all messages before 2023 as HTML
|
||||
from datetime import datetime
|
||||
|
||||
async for message in client.get_messages(chat, offset_date=datetime(2023, 1, 1)):
|
||||
print(message.sender.name, ':', message.html_text)
|
||||
|
||||
# Print the first 10 messages in a chat as markdown
|
||||
async for message in reversed(client.get_messages(chat)):
|
||||
print(message.sender.name, ':', message.markdown_text)
|
||||
"""
|
||||
return get_messages(
|
||||
self, chat, limit, offset_id=offset_id, offset_date=offset_date
|
||||
|
@ -859,9 +887,11 @@ class Client:
|
|||
"""
|
||||
Get the participants in a group or channel, along with their permissions.
|
||||
|
||||
Note that Telegram is rather strict when it comes to fetching members.
|
||||
It is very likely that you will not be able to fetch all the members.
|
||||
There is no way to bypass this.
|
||||
.. note::
|
||||
|
||||
Telegram is rather strict when it comes to fetching members.
|
||||
It is very likely that you will not be able to fetch all the members.
|
||||
There is no way to bypass this.
|
||||
|
||||
:param chat:
|
||||
The :term:`chat` to fetch participants from.
|
||||
|
@ -955,11 +985,12 @@ class Client:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
# Interactive login from the terminal
|
||||
me = await client.interactive_login()
|
||||
print('Logged in as:', me.name)
|
||||
|
||||
# or, to make sure you're logged-in as a bot
|
||||
await client.interactive_login('1234:ab56cd78ef90)
|
||||
# Automatic login to a bot account
|
||||
await client.interactive_login('54321:hJrIQtVBab0M2Yqg4HL1K-EubfY_v2fEVR')
|
||||
|
||||
.. seealso::
|
||||
|
||||
|
@ -979,6 +1010,11 @@ class Client:
|
|||
|
||||
if not await client.is_authorized():
|
||||
... # need to sign in
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`get_me` can be used to fetch up-to-date information about :term:`yourself`
|
||||
and check if you're logged-in at the same time.
|
||||
"""
|
||||
return await is_authorized(self)
|
||||
|
||||
|
@ -1075,8 +1111,7 @@ class Client:
|
|||
.. code-block:: python
|
||||
|
||||
# Mark all messages as read
|
||||
message = await client.read_message(chat, 'all')
|
||||
await message.delete()
|
||||
await client.read_message(chat, 'all')
|
||||
"""
|
||||
await read_message(self, chat, message_id)
|
||||
|
||||
|
@ -1100,18 +1135,12 @@ class Client:
|
|||
client.remove_event_handler(my_handler)
|
||||
else:
|
||||
print('still going!')
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`add_event_handler`, used to register existing functions as event handlers.
|
||||
"""
|
||||
remove_event_handler(self, handler)
|
||||
|
||||
async def request_login_code(self, phone: str) -> LoginToken:
|
||||
"""
|
||||
Request Telegram to send a login code to the provided phone number.
|
||||
This is simply the opposite of :meth:`add_event_handler`.
|
||||
Does nothing if the handler was not actually registered.
|
||||
|
||||
:param phone:
|
||||
The phone number string, in international format.
|
||||
|
@ -1126,6 +1155,11 @@ class Client:
|
|||
login_token = await client.request_login_code('+1 23 456...')
|
||||
print(login_token.timeout, 'seconds before code expires')
|
||||
|
||||
.. caution::
|
||||
|
||||
Be sure to check :meth:`is_authorized` before calling this function.
|
||||
Signing in often when you don't need to will lead to :doc:`/concepts/errors`.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`sign_in`, to complete the login procedure.
|
||||
|
@ -1159,7 +1193,7 @@ class Client:
|
|||
Resolve a username into a :term:`chat`.
|
||||
|
||||
This method is rather expensive to call.
|
||||
It is recommended to use it once and then ``chat.pack()`` the result.
|
||||
It is recommended to use it once and then :meth:`types.Chat.pack` the result.
|
||||
The packed chat can then be used (and re-fetched) more cheaply.
|
||||
|
||||
:param username:
|
||||
|
@ -1773,6 +1807,14 @@ class Client:
|
|||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
"""
|
||||
:data:`True` if :meth:`connect` has been called previously.
|
||||
|
||||
This property will be set back to :data:`False` after calling :meth:`disconnect`.
|
||||
|
||||
This property does *not* check whether the connection is alive.
|
||||
The only way to check if the connection still works is to make a request.
|
||||
"""
|
||||
return connected(self)
|
||||
|
||||
def _build_message_map(
|
||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import datetime
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Self, Tuple, Union
|
||||
|
||||
from ...session import PackedChat
|
||||
from ...tl import abcs, functions, types
|
||||
|
@ -250,37 +250,36 @@ async def forward_messages(
|
|||
|
||||
|
||||
class MessageList(AsyncList[Message]):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._reversed = False
|
||||
|
||||
def _extend_buffer(
|
||||
self, client: Client, messages: abcs.messages.Messages
|
||||
) -> Dict[int, Chat]:
|
||||
if isinstance(messages, types.messages.Messages):
|
||||
chat_map = build_chat_map(messages.users, messages.chats)
|
||||
self._buffer.extend(
|
||||
Message._from_raw(client, m, chat_map) for m in messages.messages
|
||||
)
|
||||
self._total = len(messages.messages)
|
||||
self._done = True
|
||||
return chat_map
|
||||
elif isinstance(messages, types.messages.MessagesSlice):
|
||||
chat_map = build_chat_map(messages.users, messages.chats)
|
||||
self._buffer.extend(
|
||||
Message._from_raw(client, m, chat_map) for m in messages.messages
|
||||
)
|
||||
self._total = messages.count
|
||||
return chat_map
|
||||
elif isinstance(messages, types.messages.ChannelMessages):
|
||||
chat_map = build_chat_map(messages.users, messages.chats)
|
||||
self._buffer.extend(
|
||||
Message._from_raw(client, m, chat_map) for m in messages.messages
|
||||
)
|
||||
self._total = messages.count
|
||||
return chat_map
|
||||
elif isinstance(messages, types.messages.MessagesNotModified):
|
||||
if isinstance(messages, types.messages.MessagesNotModified):
|
||||
self._total = messages.count
|
||||
return {}
|
||||
|
||||
if isinstance(messages, types.messages.Messages):
|
||||
self._total = len(messages.messages)
|
||||
self._done = True
|
||||
elif isinstance(
|
||||
messages, (types.messages.MessagesSlice, types.messages.ChannelMessages)
|
||||
):
|
||||
self._total = messages.count
|
||||
else:
|
||||
raise RuntimeError("unexpected case")
|
||||
|
||||
chat_map = build_chat_map(messages.users, messages.chats)
|
||||
self._buffer.extend(
|
||||
Message._from_raw(client, m, chat_map)
|
||||
for m in (
|
||||
reversed(messages.messages) if self._reversed else messages.messages
|
||||
)
|
||||
)
|
||||
return chat_map
|
||||
|
||||
def _last_non_empty_message(
|
||||
self,
|
||||
) -> Union[types.Message, types.MessageService, types.MessageEmpty]:
|
||||
|
@ -320,13 +319,14 @@ class HistoryList(MessageList):
|
|||
await self._client._resolve_to_packed(self._chat)
|
||||
)._to_input_peer()
|
||||
|
||||
limit = min(max(self._limit, 1), 100)
|
||||
result = await self._client(
|
||||
functions.messages.get_history(
|
||||
peer=self._peer,
|
||||
offset_id=self._offset_id,
|
||||
offset_date=self._offset_date,
|
||||
add_offset=0,
|
||||
limit=min(max(self._limit, 1), 100),
|
||||
add_offset=-limit if self._reversed else 0,
|
||||
limit=limit,
|
||||
max_id=0,
|
||||
min_id=0,
|
||||
hash=0,
|
||||
|
@ -338,9 +338,20 @@ class HistoryList(MessageList):
|
|||
self._done |= not self._limit
|
||||
if self._buffer and not self._done:
|
||||
last = self._last_non_empty_message()
|
||||
self._offset_id = self._buffer[-1].id
|
||||
if (date := getattr(last, "date", None)) is not None:
|
||||
self._offset_date = date
|
||||
self._offset_id = last.id + (1 if self._reversed else 0)
|
||||
self._offset_date = 0
|
||||
|
||||
def __reversed__(self) -> Self:
|
||||
new = self.__class__(
|
||||
self._client,
|
||||
self._chat,
|
||||
self._limit,
|
||||
offset_id=1 if self._offset_id == 0 else self._offset_id,
|
||||
offset_date=self._offset_date,
|
||||
)
|
||||
new._peer = self._peer
|
||||
new._reversed = not self._reversed
|
||||
return new
|
||||
|
||||
|
||||
def get_messages(
|
||||
|
|
|
@ -14,6 +14,7 @@ from typing import (
|
|||
|
||||
from ...session import Gap
|
||||
from ...tl import abcs
|
||||
from ..events import Continue
|
||||
from ..events import Event as EventBase
|
||||
from ..events.filters import Filter
|
||||
from ..types import build_chat_map
|
||||
|
@ -152,6 +153,6 @@ async def dispatch_next(client: Client) -> None:
|
|||
if event := event_cls._try_from_update(client, update, chat_map):
|
||||
for handler, filter in handlers:
|
||||
if not filter or filter(event):
|
||||
await handler(event)
|
||||
if client._shortcircuit_handlers:
|
||||
ret = await handler(event)
|
||||
if ret is Continue or client._shortcircuit_handlers:
|
||||
return
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from .event import Event
|
||||
from .event import Continue, Event
|
||||
from .messages import MessageDeleted, MessageEdited, MessageRead, NewMessage
|
||||
from .queries import ButtonCallback, InlineQuery
|
||||
|
||||
__all__ = [
|
||||
"Continue",
|
||||
"Event",
|
||||
"MessageDeleted",
|
||||
"MessageEdited",
|
||||
|
|
|
@ -28,3 +28,35 @@ class Event(metaclass=NoPublicConstructor):
|
|||
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
||||
) -> Optional[Self]:
|
||||
pass
|
||||
|
||||
|
||||
class Continue:
|
||||
"""
|
||||
This is **not** an event type you can listen to.
|
||||
|
||||
This is a sentinel value used to signal that the library should *Continue* calling other handlers.
|
||||
|
||||
You can :keyword:`return` this from your handlers if you want handlers registered after to also run.
|
||||
|
||||
The primary use case is having asynchronous filters inside your handler:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import events
|
||||
|
||||
@client.on(events.NewMessage)
|
||||
async def admin_only_handler(event):
|
||||
allowed = await database.is_user_admin(event.sender.id)
|
||||
if not allowed:
|
||||
# this user is not allowed, fall-through the handlers
|
||||
return events.Continue
|
||||
|
||||
@client.on(events.NewMessage)
|
||||
async def everyone_else_handler(event):
|
||||
... # runs if admin_only_handler was not allowed
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
raise TypeError(
|
||||
f"Can't instantiate {self.__class__.__name__} class (the type is the sentinel value; remove the parenthesis)"
|
||||
)
|
||||
|
|
|
@ -14,7 +14,7 @@ class NewMessage(Event, Message):
|
|||
"""
|
||||
Occurs when a new message is sent or received.
|
||||
|
||||
.. warning::
|
||||
.. caution::
|
||||
|
||||
Messages sent with the :class:`~telethon.Client` are also caught,
|
||||
so be careful not to enter infinite loops!
|
||||
|
|
|
@ -134,7 +134,7 @@ def parse(message: str) -> Tuple[str, List[MessageEntity]]:
|
|||
elif token.type in ("s_close", "s_open"):
|
||||
push(MessageEntityStrike)
|
||||
elif token.type == "softbreak":
|
||||
message += " "
|
||||
message += "\n"
|
||||
elif token.type in ("strong_close", "strong_open"):
|
||||
push(MessageEntityBold)
|
||||
elif token.type == "text":
|
||||
|
|
|
@ -105,6 +105,10 @@ class Connector(Protocol):
|
|||
default_connector = lambda ip, port: asyncio.open_connection(ip, port)
|
||||
|
||||
If your connector needs additional parameters, you can use either the :keyword:`lambda` syntax or :func:`functools.partial`.
|
||||
|
||||
.. seealso::
|
||||
|
||||
The :doc:`/concepts/datacenters` concept has examples on how to combine proxy libraries with Telethon.
|
||||
"""
|
||||
|
||||
async def __call__(self, ip: str, port: int) -> Tuple[AsyncReader, AsyncWriter]:
|
||||
|
|
|
@ -7,6 +7,7 @@ Classes related to the different event types that wrap incoming Telegram updates
|
|||
"""
|
||||
from .._impl.client.events import (
|
||||
ButtonCallback,
|
||||
Continue,
|
||||
Event,
|
||||
InlineQuery,
|
||||
MessageDeleted,
|
||||
|
@ -17,6 +18,7 @@ from .._impl.client.events import (
|
|||
|
||||
__all__ = [
|
||||
"ButtonCallback",
|
||||
"Continue",
|
||||
"Event",
|
||||
"InlineQuery",
|
||||
"MessageDeleted",
|
||||
|
|
Loading…
Reference in New Issue
Block a user