Continue documentation and matching documented behaviour

This commit is contained in:
Lonami Exo 2023-11-02 00:45:01 +01:00
parent 6e88264b28
commit 2def0a169c
21 changed files with 450 additions and 95 deletions

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.

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

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/>`_

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

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

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.

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.

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

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.

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.

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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