diff --git a/DEVELOPING.md b/DEVELOPING.md
index 43778762..56239b3a 100644
--- a/DEVELOPING.md
+++ b/DEVELOPING.md
@@ -1,22 +1,3 @@
-Code generation:
+# Developing
-```sh
-pip install -e generator/
-python tools/codegen.py
-```
-
-Formatting, type-checking and testing:
-
-```sh
-pip install -e client/[dev]
-python tools/check.py
-```
-
-Documentation (requires [sphinx](https://www.sphinx-doc.org) and [graphviz](https://www.graphviz.org)'s `dot`):
-
-```sh
-pip install -e client/[doc]
-python tools/docgen.py
-```
-
-Note that multiple optional dependency sets can be specified by separating them with a comma (`[dev,doc]`).
+See [Contributing](./client/doc/developing/contributing.rst).
diff --git a/client/doc/concepts/botapi-vs-mtproto.rst b/client/doc/concepts/botapi-vs-mtproto.rst
index f72c71d1..e8459c3f 100644
--- a/client/doc/concepts/botapi-vs-mtproto.rst
+++ b/client/doc/concepts/botapi-vs-mtproto.rst
@@ -352,7 +352,8 @@ For the most part, it's a 1-to-1 translation and the result is idiomatic Teletho
Migrating from aiogram
-``````````````````````
+^^^^^^^^^^^^^^^^^^^^^^
+
Using one of the examples from their v3 documentation with logging and comments removed:
.. code-block:: python
diff --git a/client/doc/concepts/full-api.rst b/client/doc/concepts/full-api.rst
index 0bfd75c8..572a5a16 100644
--- a/client/doc/concepts/full-api.rst
+++ b/client/doc/concepts/full-api.rst
@@ -55,7 +55,7 @@ To check for a concrete type, you can use :func:`isinstance`:
if isinstance(invite, tl.types.ChatInviteAlready):
print(invite.chat)
-The ``telethon._tl`` module is not documented here because it would result in tens of megabytes.
+The ``telethon._tl`` module is not documented here because it would greatly bloat the documentation and make search harder.
Instead, there are multiple alternatives:
* Use Telethon's separate site to search in the `Telethon Raw API `_.
diff --git a/client/doc/concepts/glossary.rst b/client/doc/concepts/glossary.rst
index 784ad047..e5b5904d 100644
--- a/client/doc/concepts/glossary.rst
+++ b/client/doc/concepts/glossary.rst
@@ -66,3 +66,5 @@ Glossary
Type Language
File format used by Telegram to define all the types and requests available in a :term:`layer`.
Telegram's site has an `Overview of the TL language `_.
+
+ .. seealso:: :ref:`Type Language brief `.
diff --git a/client/doc/concepts/messages.rst b/client/doc/concepts/messages.rst
index c0539136..76b2177d 100644
--- a/client/doc/concepts/messages.rst
+++ b/client/doc/concepts/messages.rst
@@ -15,6 +15,8 @@ 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.
+.. _formatting:
+
Formatting messages
-------------------
diff --git a/client/doc/conf.py b/client/doc/conf.py
index 38bd3a59..c289a6ef 100644
--- a/client/doc/conf.py
+++ b/client/doc/conf.py
@@ -23,6 +23,7 @@ extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.graphviz",
+ "sphinx.ext.coverage",
"roles.tl",
]
@@ -31,7 +32,9 @@ tl_ref_url = "https://tl.telethon.dev"
autodoc_default_options = {
"members": True,
"undoc-members": True,
+ "show-inheritance": True,
}
+autodoc_typehints = "description"
modindex_common_prefix = ["telethon."]
graphviz_output_format = "svg"
diff --git a/client/doc/developing/coding-style.rst b/client/doc/developing/coding-style.rst
deleted file mode 100644
index fd9fce04..00000000
--- a/client/doc/developing/coding-style.rst
+++ /dev/null
@@ -1,17 +0,0 @@
-Coding style
-============
-
-Knowledge of Python is a obviously a must to develop a Python library.
-A good online resource is `Dive Into Python 3 `_.
-
-Telethon uses multiple tools to automatically format the code and check for linting rules.
-This means you can simply ignore formatting and let the tools handle it for you.
-You can find these tools under the ``tools/`` folder.
-
-The documentation is written with mostly a newline after every period.
-This is not a hard rule.
-Lines can be cut earlier if they become too long to be comfortable.
-
-Commit messages should be short and descriptive.
-They should start with an action in the present ("Fix" and not "Fixed").
-This saves a few characters and represents what the commit will "do" after applied.
diff --git a/client/doc/developing/project-structure.rst b/client/doc/developing/contributing.rst
similarity index 62%
rename from client/doc/developing/project-structure.rst
rename to client/doc/developing/contributing.rst
index 1821ddd5..d78b26b8 100644
--- a/client/doc/developing/project-structure.rst
+++ b/client/doc/developing/contributing.rst
@@ -1,5 +1,50 @@
-Project Structure
-=================
+Contributing
+============
+
+Telethon welcomes all new contributions, whether it's reporting bugs or sending code patches.
+
+Please keep both the philosophy and coding style below in mind.
+
+Be mindful when adding new features.
+Every new feature must be understood by the maintainer, or otherwise it will probably rot.
+The *usefulness : maintenance-cost* ratio must be high enough to warrant being built-in.
+Consider whether your new features could be a separate add-on project entirely.
+
+
+Philosophy
+----------
+
+* Dependencies should only be added when absolutely necessary.
+* Dependencies written in anything other than Python cannot be mandatory.
+* The library must work correctly with no system dependencies other than Python 3.
+* Strict type-checking is required to pass everywhere in the library to make upgrades easier.
+* The code structure must make use of hard and clear boundaries to keep the different parts decoupled.
+* The API should cover only the most commonly used features to avoid bloat and reduce maintenance costs.
+* Documentation must be a pleasure to use and contain plenty of code examples.
+
+
+Coding style
+------------
+
+Knowledge of Python is a obviously a must to develop a Python library.
+A good online resource is `Dive Into Python 3 `_.
+
+Telethon uses multiple tools to automatically format the code and check for linting rules.
+This means you can simply ignore formatting and let the tools handle it for you.
+You can find these tools under the ``tools/`` folder.
+See :ref:`tools` below for an explanation.
+
+The documentation is written with mostly a newline after every period.
+This is not a hard rule.
+Lines can be cut earlier if they become too long to be comfortable.
+
+Commit messages should be short and descriptive.
+They should start with an action in the present ("Fix" and not "Fixed").
+This saves a few characters and represents what the commit will "do" after applied.
+
+
+Project structure
+-----------------
.. currentmodule:: telethon
@@ -7,29 +52,61 @@ The repository contains several folders, each with their own "package".
benches/
---------
+^^^^^^^^
This folder contains different benchmarks.
Pretty straightforward.
stubs/
-------
+^^^^^^
If a dependency doesn't support typing, files here must work around that.
+.. _tools:
tools/
-------
+^^^^^^
Various utility scripts.
Each script should have a "comment" at the top explaining what they are for.
-See ``DEVELOPING.md`` in the repository root to learn how to use some of the tools.
+Code generation
+"""""""""""""""
+This will take ``api.tl`` and ``mtproto.tl`` files and generate ``client/_impl/tl``.
+
+.. code-block:: sh
+
+ pip install -e generator/
+ python tools/codegen.py
+
+Linting
+"""""""
+
+This includes format checks, type-checking and testing.
+
+.. code-block:: sh
+
+ pip install -e client/[dev]
+ python tools/check.py
+
+Documentation
+"""""""""""""
+
+Requires `sphinx `_ and `graphviz `_'s ``dot``.
+
+.. code-block:: sh
+
+ pip install -e client/[doc]
+ python tools/docgen.py
+
+Note that multiple optional dependency sets can be specified by separating them with a comma (``[dev,doc]``).
+
+.. _tl-brief:
generator/
-----------
+^^^^^^^^^^
A package that should not be published and is only used when developing the library.
The implementation is private and exists under the ``src/*/_impl/`` folder.
@@ -72,11 +149,11 @@ An in-memory "filesystem" structure is kept before writing all files to disk.
This makes it possible to execute most of the process in a sans-io manner.
Once the code generation finishes, all files are written to disk at once.
-See ``DEVELOPING.md`` in the repository root to learn how to generate code.
+See :ref:`tools` above to learn how to generate code.
client/
--------
+^^^^^^^
The Telethon client library and documentation lives here.
This is the package that gets published.
diff --git a/client/doc/developing/migration-guide.rst b/client/doc/developing/migration-guide.rst
index 4ad28959..557cd6a5 100644
--- a/client/doc/developing/migration-guide.rst
+++ b/client/doc/developing/migration-guide.rst
@@ -64,6 +64,21 @@ This was also a good opportunity to remove a lot of modules that were not suppos
``.crypto``, ``.extensions``, ``.network``, ``.custom``, ``.functions``, ``.helpers``, ``.hints``, ``.password``, ``.requestiter``, ``.sync``, ``.types``, ``.utils``.
+TelegramClient renamed to Client
+--------------------------------
+
+You can rename it with :keyword:`as` during import if you want to use the old name.
+
+Python allows using namespaces via packages and modules.
+Therefore, the full name :class:`telethon.Client` already indicates it's from ``telethon``, so the old ``Telegram`` prefix was redundant.
+
+
+No telethon.sync hack
+---------------------
+
+You can no longer ``import telethon.sync`` to have most calls wrapped in :meth:`asyncio.loop.run_until_complete` for you.
+
+
Raw API is now private
----------------------
@@ -140,6 +155,27 @@ Functions no longer have an asynchronous ``.resolve()``.
This used to let you pass usernames and have them be resolved to :tl:`InputPeer` automatically (unless it was nested).
+Changes to start and client context-manager
+-------------------------------------------
+
+You can no longer ``start()`` the client.
+
+Instead, you will need to first :meth:`~Client.connect` and then start the :meth:`~Client.interactive_login`.
+
+In v1, the when using the client as a context-manager, ``start()`` was called.
+Since that method no longer exists, it now instead only :meth:`~Client.connect` and :meth:`~Client.disconnect`.
+
+This means you won't get annoying prompts in your terminal if the session was not authorized.
+It also means you can now use the context manager even with custom login flows.
+
+The old ``sign_in()`` method also sent the code, which was rather confusing.
+Instead, you must now :meth:`~Client.request_login_code` as a separate operation.
+
+The old ``log_out()`` was also renamed to :meth:`~Client.sign_out` for consistency with :meth:`~Client.sign_in`.
+
+The old ``is_user_authorized()`` was renamed to :meth:`~Client.is_authorized` since it works for bot accounts too.
+
+
Unified client iter and get methods
-----------------------------------
@@ -451,38 +487,3 @@ StringSession no longer exists
If you need to serialize the session data to a string, you can use something like `jsonpickle `_.
Or even the built-in :mod:`pickle` followed by :mod:`base64` or just :meth:`bytes.hex`.
But be aware that these approaches probably will not be compatible with additions to the :class:`~session.Session`.
-
-
-TelegramClient renamed to Client
---------------------------------
-
-You can rename it with :keyword:`as` during import if you want to use the old name.
-
-Python allows using namespaces via packages and modules.
-Therefore, the full name :class:`telethon.Client` already indicates it's from ``telethon``, so the old ``Telegram`` prefix was redundant.
-
-
-Changes to start and client context-manager
--------------------------------------------
-
-You can no longer ``start()`` the client.
-
-Instead, you will need to first :meth:`~Client.connect` and then start the :meth:`~Client.interactive_login`.
-
-In v1, the when using the client as a context-manager, ``start()`` was called.
-Since that method no longer exists, it now instead only :meth:`~Client.connect` and :meth:`~Client.disconnect`.
-
-This means you won't get annoying prompts in your terminal if the session was not authorized.
-It also means you can now use the context manager even with custom login flows.
-
-The old ``sign_in()`` method also sent the code, which was rather confusing.
-Instead, you must now :meth:`~Client.request_login_code` as a separate operation.
-
-The old ``log_out()`` was also renamed to :meth:`~Client.sign_out` for consistency with :meth:`~Client.sign_in`.
-
-The old ``is_user_authorized()`` was renamed to :meth:`~Client.is_authorized` since it works for bot accounts too.
-
-No telethon.sync hack
----------------------
-
-You can no longer ``import telethon.sync`` to have most calls wrapped in :meth:`asyncio.loop.run_until_complete` for you.
diff --git a/client/doc/developing/philosophy.rst b/client/doc/developing/philosophy.rst
deleted file mode 100644
index 435d6150..00000000
--- a/client/doc/developing/philosophy.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-Philosophy
-==========
-
-* Dependencies should only be added when absolutely necessary.
-* Dependencies written in anything other than Python cannot be mandatory.
-* The library must work correctly with no system dependencies other than Python 3.
-* Strict type-checking is required to pass everywhere in the library to make upgrades easier.
-* The code structure must make use of hard and clear boundaries to keep the different parts decoupled.
-* The API should cover only the most commonly used features to avoid bloat and reduce maintenance costs.
-* Documentation must be a pleasure to use and contain plenty of code examples.
diff --git a/client/doc/index.rst b/client/doc/index.rst
index a3d7dc2e..e1b32122 100644
--- a/client/doc/index.rst
+++ b/client/doc/index.rst
@@ -123,6 +123,4 @@ Tips and tricks to develop both with the library and for the library.
developing/changelog
developing/migration-guide
developing/faq
- developing/philosophy.rst
- developing/coding-style.rst
- developing/project-structure.rst
+ developing/contributing
diff --git a/client/doc/modules/client.rst b/client/doc/modules/client.rst
index ac028519..7df92f57 100644
--- a/client/doc/modules/client.rst
+++ b/client/doc/modules/client.rst
@@ -1,4 +1 @@
-Client
-======
-
.. autoclass:: telethon.Client
diff --git a/client/doc/modules/sessions.rst b/client/doc/modules/sessions.rst
index 6426e19a..aedfcd80 100644
--- a/client/doc/modules/sessions.rst
+++ b/client/doc/modules/sessions.rst
@@ -1,4 +1,20 @@
-Session storages
-================
+Sessions
+========
-.. automodule:: telethon.session
+.. currentmodule:: telethon.session
+
+Storages
+--------
+
+.. autoclass:: Storage
+.. autoclass:: SqliteSession
+.. autoclass:: MemorySession
+
+Types
+-----
+
+.. autoclass:: Session
+.. autoclass:: DataCenter
+.. autoclass:: User
+.. autoclass:: UpdateState
+.. autoclass:: ChannelState
diff --git a/client/src/telethon/__init__.py b/client/src/telethon/__init__.py
index fb1528e9..2ad99319 100644
--- a/client/src/telethon/__init__.py
+++ b/client/src/telethon/__init__.py
@@ -1,8 +1,10 @@
+"""
+The main package for the Telethon library.
+"""
from ._impl import tl as _tl
-from ._impl.client import Client, Config
+from ._impl.client import Client
from ._impl.client.errors import errors
from ._impl.mtproto import RpcError
-from ._impl.session import Session
from .version import __version__
-__all__ = ["_tl", "Client", "Config", "errors", "RpcError", "Session"]
+__all__ = ["_tl", "Client", "errors", "RpcError"]
diff --git a/client/src/telethon/_impl/client/client/auth.py b/client/src/telethon/_impl/client/client/auth.py
index 915352fa..baf4a927 100644
--- a/client/src/telethon/_impl/client/client/auth.py
+++ b/client/src/telethon/_impl/client/client/auth.py
@@ -9,7 +9,7 @@ from ...mtproto import RpcError
from ...session import User as SessionUser
from ...tl import abcs, functions, types
from ..types import LoginToken, PasswordToken, User
-from .net import connect_sender
+from .net import connect_sender, datacenter_for_id
if TYPE_CHECKING:
from .client import Client
@@ -26,11 +26,12 @@ async def is_authorized(self: Client) -> bool:
async def complete_login(client: Client, auth: abcs.auth.Authorization) -> User:
+ assert client._sender
assert isinstance(auth, types.auth.Authorization)
assert isinstance(auth.user, types.User)
user = User._from_raw(auth.user)
- client._config.session.user = SessionUser(
- id=user.id, dc=client._dc_id, bot=user.bot, username=user.username
+ client._session.user = SessionUser(
+ id=user.id, dc=client._sender.dc_id, bot=user.bot, username=user.username
)
packed = user.pack()
@@ -48,10 +49,11 @@ async def complete_login(client: Client, auth: abcs.auth.Authorization) -> User:
async def handle_migrate(client: Client, dc_id: Optional[int]) -> None:
assert dc_id is not None
- sender = await connect_sender(dc_id, client._config)
+ sender, client._session.dcs = await connect_sender(
+ client._config, datacenter_for_id(client, dc_id)
+ )
async with client._sender_lock:
client._sender = sender
- client._dc_id = dc_id
async def bot_sign_in(self: Client, token: str) -> User:
diff --git a/client/src/telethon/_impl/client/client/bots.py b/client/src/telethon/_impl/client/client/bots.py
index 42271483..8b47ab30 100644
--- a/client/src/telethon/_impl/client/client/bots.py
+++ b/client/src/telethon/_impl/client/client/bots.py
@@ -1,9 +1,9 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, AsyncIterator, List, Optional, Self, Union
+from typing import TYPE_CHECKING, AsyncIterator, List, Optional, Self
from ...tl import abcs, functions, types
-from ..types import ChatLike, Message, NoPublicConstructor
+from ..types import ChatLike, InlineResult, NoPublicConstructor
from ..utils import generate_random_id
if TYPE_CHECKING:
@@ -64,64 +64,6 @@ class InlineResults(metaclass=NoPublicConstructor):
return self._buffer.pop()
-class InlineResult(metaclass=NoPublicConstructor):
- def __init__(
- self,
- client: Client,
- results: types.messages.BotResults,
- result: Union[types.BotInlineMediaResult, types.BotInlineResult],
- default_peer: abcs.InputPeer,
- ):
- self._client = client
- self._raw_results = results
- self._raw = result
- self._default_peer = default_peer
-
- @property
- def type(self) -> str:
- return self._raw.type
-
- @property
- def title(self) -> str:
- return self._raw.title or ""
-
- @property
- def description(self) -> Optional[str]:
- return self._raw.description
-
- async def send(
- self,
- chat: Optional[ChatLike],
- ) -> Message:
- if chat is None and isinstance(self._default_peer, types.InputPeerEmpty):
- raise ValueError("no target chat was specified")
-
- if chat is not None:
- peer = (await self._client._resolve_to_packed(chat))._to_input_peer()
- else:
- peer = self._default_peer
-
- random_id = generate_random_id()
- return self._client._build_message_map(
- await self._client(
- functions.messages.send_inline_bot_result(
- silent=False,
- background=False,
- clear_draft=False,
- hide_via=False,
- peer=peer,
- reply_to=None,
- random_id=random_id,
- query_id=self._raw_results.query_id,
- id=self._raw.id,
- schedule_date=None,
- send_as=None,
- )
- ),
- peer,
- ).with_random_id(random_id)
-
-
async def inline_query(
self: Client, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None
) -> AsyncIterator[InlineResult]:
diff --git a/client/src/telethon/_impl/client/client/client.py b/client/src/telethon/_impl/client/client/client.py
index 1de75c9b..54a4c111 100644
--- a/client/src/telethon/_impl/client/client/client.py
+++ b/client/src/telethon/_impl/client/client/client.py
@@ -18,6 +18,9 @@ from typing import (
Union,
)
+from telethon._impl.session.session import DataCenter
+
+from ....version import __version__ as default_version
from ...mtsender import Sender
from ...session import (
ChatHashCache,
@@ -93,6 +96,8 @@ from .net import (
Config,
connect,
connected,
+ default_device_model,
+ default_system_version,
disconnect,
invoke_request,
run_until_disconnected,
@@ -137,9 +142,13 @@ class Client:
:param api_id:
The API ID. See :doc:`/basic/signing-in` to learn how to obtain it.
+ This is required to initialize the connection.
+
:param api_hash:
The API hash. See :doc:`/basic/signing-in` to learn how to obtain it.
+ This is required to sign in, and can be omitted otherwise.
+
:param device_model:
Device model.
@@ -158,8 +167,8 @@ class Client:
:param catch_up:
Whether to "catch up" on updates that occured while the client was not connected.
- :param server_addr:
- Override the server address ``'ip:port'`` pair to connect to.
+ :param datacenter:
+ Override the datacenter to connect to.
Useful to connect to one of Telegram's test servers.
:param flood_sleep_threshold:
@@ -183,23 +192,45 @@ class Client:
session: Optional[Union[str, Path, Storage]],
api_id: int,
api_hash: Optional[str] = None,
+ *,
+ device_model: Optional[str] = None,
+ system_version: Optional[str] = None,
+ app_version: Optional[str] = None,
+ system_lang_code: Optional[str] = None,
+ lang_code: Optional[str] = None,
+ catch_up: Optional[bool] = None,
+ datacenter: Optional[DataCenter] = None,
+ flood_sleep_threshold: Optional[int] = None,
+ update_queue_limit: Optional[int] = None,
check_all_handlers: bool = False,
) -> None:
self._sender: Optional[Sender] = None
self._sender_lock = asyncio.Lock()
- self._dc_id = DEFAULT_DC
if isinstance(session, Storage):
self._storage = session
elif session is None:
self._storage = MemorySession()
else:
self._storage = SqliteSession(session)
+
self._config = Config(
- session=Session(),
api_id=api_id,
api_hash=api_hash or "",
+ device_model=device_model or default_device_model(),
+ system_version=system_version or default_system_version(),
+ app_version=app_version or default_version,
+ system_lang_code=system_lang_code or "en",
+ lang_code=lang_code or "en",
+ catch_up=catch_up or False,
+ datacenter=datacenter,
+ flood_sleep_threshold=60
+ if flood_sleep_threshold is None
+ else flood_sleep_threshold,
+ update_queue_limit=update_queue_limit,
)
+ self._session = Session()
+
self._message_box = MessageBox()
self._chat_hashes = ChatHashCache(None)
self._last_update_limit_warn: Optional[float] = None
@@ -212,10 +243,8 @@ class Client:
] = {}
self._shortcircuit_handlers = not check_all_handlers
- if self_user := self._config.session.user:
- self._dc_id = self_user.dc
- if self._config.catch_up and self._config.session.state:
- self._message_box.load(self._config.session.state)
+ if self._session.user and self._config.catch_up and self._session.state:
+ self._message_box.load(self._session.state)
# Begin partially @generated
@@ -468,7 +497,10 @@ class Client:
:param message_id:
The identifier of the message to edit.
- The rest of parameters behave the same as they do in `send_message` or `send_file`.
+ :param text: See :ref:`formatting `.
+ :param markdown: See :ref:`formatting `.
+ :param html: See :ref:`formatting `.
+ :param link_preview: See :ref:`formatting `.
:return: The edited message.
@@ -728,6 +760,27 @@ class Client:
def get_messages_with_ids(
self, chat: ChatLike, message_ids: List[int]
) -> AsyncList[Message]:
+ """
+ Get the full message objects from the corresponding message identifiers.
+
+ :param chat:
+ The :term:`chat` where the message to fetch is.
+
+ :param message_ids:
+ The message identifiers of the messages to fetch.
+
+ :return:
+ The matching messages.
+ The order of the returned messages is *not* guaranteed to match the input.
+ The method may return less messages than requested when some are missing.
+
+ .. rubric:: Example
+
+ .. code-block:: python
+
+ # Get the first message (after "Channel created") of the chat
+ first_message = (await client.get_messages_with_ids(chat, [2]))[0]
+ """
return get_messages_with_ids(self, chat, message_ids)
def get_participants(self, chat: ChatLike) -> AsyncList[Participant]:
@@ -738,6 +791,9 @@ class Client:
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.
+
:return: The participants.
.. rubric:: Example
@@ -753,6 +809,9 @@ class Client:
"""
Get the profile pictures set in a chat, or user avatars.
+ :param chat:
+ The :term:`chat` to fetch the profile photo files from.
+
:return: The photo files.
.. rubric:: Example
@@ -990,9 +1049,32 @@ class Client:
return await resolve_to_packed(self, chat)
async def resolve_username(self, username: str) -> Chat:
+ """
+ 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.
+ The packed chat can then be used (and re-fetched) more cheaply.
+
+ :param username:
+ The public "@username" to resolve.
+
+ :return: The matching chat.
+
+ .. rubric:: Example
+
+ .. code-block:: python
+
+ print(await client.resolve_username('@cat'))
+ """
return await resolve_username(self, username)
async def run_until_disconnected(self) -> None:
+ """
+ Keep running the library until a disconnection occurs.
+
+ Connection errors will be raised from this method if they occur.
+ """
await run_until_disconnected(self)
def search_all_messages(
@@ -1084,10 +1166,8 @@ class Client:
async def send_audio(
self,
chat: ChatLike,
- path: Optional[Union[str, Path, File]] = None,
+ file: Union[str, Path, InFileLike, File],
*,
- url: Optional[str] = None,
- file: Optional[InFileLike] = None,
size: Optional[int] = None,
name: Optional[str] = None,
duration: Optional[float] = None,
@@ -1105,12 +1185,18 @@ class Client:
duration, title and performer if they are not provided.
:param chat:
- The :term:`chat` where the message will be sent to.
+ The :term:`chat` where the audio media will be sent to.
- :param path:
- A local file path or :class:`~telethon.types.File` to send.
-
- The rest of parameters behave the same as they do in :meth:`send_file`.
+ :param file: See :meth:`send_file`.
+ :param size: See :meth:`send_file`.
+ :param name: See :meth:`send_file`.
+ :param duration: See :meth:`send_file`.
+ :param voice: See :meth:`send_file`.
+ :param title: See :meth:`send_file`.
+ :param performer: See :meth:`send_file`.
+ :param caption: See :ref:`formatting`.
+ :param caption_markdown: See :ref:`formatting`.
+ :param caption_html: See :ref:`formatting`.
.. rubric:: Example
@@ -1121,9 +1207,7 @@ class Client:
return await send_audio(
self,
chat,
- path,
- url=url,
- file=file,
+ file,
size=size,
name=name,
duration=duration,
@@ -1138,10 +1222,8 @@ class Client:
async def send_file(
self,
chat: ChatLike,
- path: Optional[Union[str, Path, File]] = None,
+ file: Union[str, Path, InFileLike, File],
*,
- url: Optional[str] = None,
- file: Optional[InFileLike] = None,
size: Optional[int] = None,
name: Optional[str] = None,
mime_type: Optional[str] = None,
@@ -1152,7 +1234,6 @@ class Client:
title: Optional[str] = None,
performer: Optional[str] = None,
emoji: Optional[str] = None,
- emoji_sticker: Optional[str] = None,
width: Optional[int] = None,
height: Optional[int] = None,
round: bool = False,
@@ -1169,7 +1250,7 @@ class Client:
If you want to let the library attempt to guess the file metadata, use the type-specific methods to send media:
`send_photo`, `send_audio` or `send_file`.
- Unlike `send_photo`, image files will be sent as documents by default.
+ Unlike :meth:`send_photo`, image files will be sent as documents by default.
:param chat:
The :term:`chat` where the message will be sent to.
@@ -1177,36 +1258,121 @@ class Client:
:param path:
A local file path or :class:`~telethon.types.File` to send.
- :param caption:
- Caption text to display under the media, with no formatting.
+ :param file:
+ The file to send.
- :param caption_markdown:
- Caption text to display under the media, parsed as markdown.
+ This can be a path, relative or absolute, to a local file, as either a :class:`str` or :class:`pathlib.Path`.
- :param caption_html:
- Caption text to display under the media, parsed as HTML.
+ It can also be a file opened for reading in binary mode, with its ``read`` method optionally being ``async``.
+ Note that the file descriptor will *not* be seeked back to the start before sending it.
- The rest of parameters are passed to :meth:`telethon.types.File.new`
- if *path* isn't a :class:`~telethon.types.File`.
- See the documentation of :meth:`~telethon.types.File.new` to learn what they do.
+ If you wrote to an in-memory file, you probably want to ``file.seek(0)`` first.
+ If you want to send :class:`bytes`, wrap them in :class:`io.BytesIO` first.
- See the section on :doc:`/concepts/messages` to learn about message formatting.
+ You can also pass any :class:`~telethon.types.File` that was previously sent in Telegram to send a copy.
+ This will not download and re-upload the file, but will instead reuse the original without forwarding it.
- Note that only one *caption* parameter can be provided.
+ Last, a URL can also be specified.
+ For the library to detect it as a URL, the string *must* start with either ``http://` or ``https://``.
+ Telethon will *not* download and upload the file, but will instead pass the URL to Telegram.
+ If Telegram is unable to access the media, is too large, or is invalid, the method will fail.
+
+ When using URLs, it is recommended to explicitly pass either a name or define the mime-type.
+ To make sure the URL is interpreted as an image, use `send_photo`.
+
+ :param size:
+ The size of the local file to send.
+
+ This parameter **must** be specified when sending a previously-opened or in-memory files.
+ The library will not ``seek`` the file to attempt to determine the size.
+
+ This can be less than the real file size, in which case only ``size`` bytes will be sent.
+ This can be useful if you have a single buffer with multiple files.
+
+ :param name:
+ Override for the default file name.
+
+ When given a string or path, the :attr:`pathlib.Path.name` will be used by default only if this parameter is omitted.
+
+ This parameter **must** be specified when sending a previously-opened or in-memory files.
+ The library will not attempt to read any ``name`` attributes the object may have.
+
+ :param mime_type:
+ Override for the default mime-type.
+
+ By default, the library will use :func:`mimetypes.guess_type` on the name.
+
+ If no mime-type is registered for the name's extension, ``application/octet-stream`` will be used.
+
+ :param compress:
+ Whether the image file is allowed to be compressed by Telegram.
+
+ If not, image files will be sent as document.
+
+ :param animated:
+ Whether the sticker is animated (not a static image).
+
+ :param duration:
+ Duration, in seconds, of the audio or video.
+
+ This field should be specified when sending audios or videos from local files.
+
+ The floating-point value will be rounded to an integer.
+
+ :param voice:
+ Whether the audio is a live recording, often recorded right before sending it.
+
+ :param title:
+ Title of the song in the audio file.
+
+ :param performer:
+ Artist or main performer of the song in the audio file.
+
+ :param emoji:
+ Alternative text for the sticker.
+
+ :param width:
+ Width, in pixels, of the image or video.
+
+ This field should be specified when sending images or videos from local files.
+
+ :param height:
+ Height, in pixels, of the image or video.
+
+ This field should be specified when sending images or videos from local files.
+
+ :param round:
+ Whether the video should be displayed as a round video.
+
+ :param supports_streaming:
+ Whether clients are allowed to stream the video having to wait for a full download.
+
+ Note that the file format of the video must have streaming support.
+
+ :param muted:
+ Whether the sound of the video is or should be missing.
+
+ This is often used for short animations or "GIFs".
+
+ :param caption: See :ref:`formatting`.
+ :param caption_markdown: See :ref:`formatting`.
+ :param caption_html: See :ref:`formatting`.
.. rubric:: Example
.. code-block:: python
- login_token = await client.request_login_code('+1 23 456...')
- print(login_token.timeout, 'seconds before code expires')
+ await client.send_file(chat, 'picture.jpg')
+
+ # Sending in-memory bytes
+ import io
+ data = b'my in-memory document'
+ cawait client.send_file(chat, io.BytesIO(data), size=len(data), name='doc.txt')
"""
return await send_file(
self,
chat,
- path,
- url=url,
- file=file,
+ file,
size=size,
name=name,
mime_type=mime_type,
@@ -1217,7 +1383,6 @@ class Client:
title=title,
performer=performer,
emoji=emoji,
- emoji_sticker=emoji_sticker,
width=width,
height=height,
round=round,
@@ -1244,16 +1409,9 @@ class Client:
:param chat:
The :term:`chat` where the message will be sent to.
- :param text:
- Message text, with no formatting.
-
- When given a :class:`Message` instance, a copy of the message will be sent.
-
- :param text_markdown:
- Message text, parsed as CommonMark.
-
- :param text_html:
- Message text, parsed as HTML.
+ :param text: See :ref:`formatting`.
+ :param markdown: See :ref:`formatting`.
+ :param html: See :ref:`formatting`.
:param link_preview:
Whether the link preview is allowed.
@@ -1266,10 +1424,6 @@ class Client:
:param reply_to:
The message identifier of the message to reply to.
- Note that exactly one *text* parameter must be provided.
-
- See the section on :doc:`/concepts/messages` to learn about message formatting.
-
.. rubric:: Example
.. code-block:: python
@@ -1289,10 +1443,8 @@ class Client:
async def send_photo(
self,
chat: ChatLike,
- path: Optional[Union[str, Path, File]] = None,
+ file: Union[str, Path, InFileLike, File],
*,
- url: Optional[str] = None,
- file: Optional[InFileLike] = None,
size: Optional[int] = None,
name: Optional[str] = None,
compress: bool = True,
@@ -1309,16 +1461,21 @@ class Client:
Only compressed images can be displayed as photos in applications.
If *compress* is set to :data:`False`, the image will be sent as a file document.
- Unlike `send_file`, this method will attempt to guess the values for
+ Unlike :meth:`send_file`, this method will attempt to guess the values for
width and height if they are not provided.
:param chat:
- The :term:`chat` where the message will be sent to.
+ The :term:`chat` where the photo media will be sent to.
- :param path:
- A local file path or :class:`~telethon.types.File` to send.
-
- The rest of parameters behave the same as they do in :meth:`send_file`.
+ :param file: See :meth:`send_file`.
+ :param size: See :meth:`send_file`.
+ :param name: See :meth:`send_file`.
+ :param compress: See :meth:`send_file`.
+ :param width: See :meth:`send_file`.
+ :param height: See :meth:`send_file`.
+ :param caption: See :ref:`formatting`.
+ :param caption_markdown: See :ref:`formatting`.
+ :param caption_html: See :ref:`formatting`.
.. rubric:: Example
@@ -1329,9 +1486,7 @@ class Client:
return await send_photo(
self,
chat,
- path,
- url=url,
- file=file,
+ file,
size=size,
name=name,
compress=compress,
@@ -1345,10 +1500,8 @@ class Client:
async def send_video(
self,
chat: ChatLike,
- path: Optional[Union[str, Path, File]] = None,
+ file: Union[str, Path, InFileLike, File],
*,
- url: Optional[str] = None,
- file: Optional[InFileLike] = None,
size: Optional[int] = None,
name: Optional[str] = None,
duration: Optional[float] = None,
@@ -1363,16 +1516,23 @@ class Client:
"""
Send a video file.
- Unlike `send_file`, this method will attempt to guess the values for
+ Unlike :meth:`send_file`, this method will attempt to guess the values for
duration, width and height if they are not provided.
:param chat:
The :term:`chat` where the message will be sent to.
- :param path:
- A local file path or :class:`~telethon.types.File` to send.
-
- The rest of parameters behave the same as they do in :meth:`send_file`.
+ :param file: See :meth:`send_file`.
+ :param size: See :meth:`send_file`.
+ :param name: See :meth:`send_file`.
+ :param duration: See :meth:`send_file`.
+ :param width: See :meth:`send_file`.
+ :param height: See :meth:`send_file`.
+ :param round: See :meth:`send_file`.
+ :param supports_streaming: See :meth:`send_file`.
+ :param caption: See :ref:`formatting`.
+ :param caption_markdown: See :ref:`formatting`.
+ :param caption_html: See :ref:`formatting`.
.. rubric:: Example
@@ -1383,9 +1543,7 @@ class Client:
return await send_video(
self,
chat,
- path,
- url=url,
- file=file,
+ file,
size=size,
name=name,
duration=duration,
@@ -1442,6 +1600,10 @@ class Client:
:param token:
The login token returned from :meth:`request_login_code`.
+ :param code:
+ The login code sent by Telegram to a previously-authorized device.
+ This should be a short string of digits.
+
:return:
The user corresponding to :term:`yourself`, or a password token if the account has 2FA enabled.
diff --git a/client/src/telethon/_impl/client/client/files.py b/client/src/telethon/_impl/client/client/files.py
index fd9e30a4..da2eafc0 100644
--- a/client/src/telethon/_impl/client/client/files.py
+++ b/client/src/telethon/_impl/client/client/files.py
@@ -1,9 +1,12 @@
from __future__ import annotations
import hashlib
+import math
+import mimetypes
+import urllib.parse
from inspect import isawaitable
from pathlib import Path
-from typing import TYPE_CHECKING, Optional, Union
+from typing import TYPE_CHECKING, List, Optional, Union
from ...tl import abcs, functions, types
from ..types import (
@@ -28,14 +31,15 @@ MAX_CHUNK_SIZE = 512 * 1024
FILE_MIGRATE_ERROR = 303
BIG_FILE_SIZE = 10 * 1024 * 1024
+# ``round`` parameter would make this more annoying to access otherwise.
+math_round = round
+
async def send_photo(
self: Client,
chat: ChatLike,
- path: Optional[Union[str, Path, File]] = None,
+ file: Union[str, Path, InFileLike, File],
*,
- url: Optional[str] = None,
- file: Optional[InFileLike] = None,
size: Optional[int] = None,
name: Optional[str] = None,
compress: bool = True,
@@ -48,9 +52,7 @@ async def send_photo(
return await send_file(
self,
chat,
- path,
- url=url,
- file=file,
+ file,
size=size,
name=name,
compress=compress,
@@ -65,10 +67,8 @@ async def send_photo(
async def send_audio(
self: Client,
chat: ChatLike,
- path: Optional[Union[str, Path, File]] = None,
+ file: Union[str, Path, InFileLike, File],
*,
- url: Optional[str] = None,
- file: Optional[InFileLike] = None,
size: Optional[int] = None,
name: Optional[str] = None,
duration: Optional[float] = None,
@@ -82,9 +82,7 @@ async def send_audio(
return await send_file(
self,
chat,
- path,
- url=url,
- file=file,
+ file,
size=size,
name=name,
duration=duration,
@@ -100,10 +98,8 @@ async def send_audio(
async def send_video(
self: Client,
chat: ChatLike,
- path: Optional[Union[str, Path, File]] = None,
+ file: Union[str, Path, InFileLike, File],
*,
- url: Optional[str] = None,
- file: Optional[InFileLike] = None,
size: Optional[int] = None,
name: Optional[str] = None,
duration: Optional[float] = None,
@@ -118,9 +114,7 @@ async def send_video(
return await send_file(
self,
chat,
- path,
- url=url,
- file=file,
+ file,
size=size,
name=name,
duration=duration,
@@ -134,13 +128,20 @@ async def send_video(
)
+def try_get_url_path(maybe_url: Union[str, Path, InFileLike]) -> Optional[str]:
+ if not isinstance(maybe_url, str):
+ return None
+ lowercase = maybe_url.lower()
+ if lowercase.startswith("http://") or lowercase.startswith("https://"):
+ return urllib.parse.urlparse(maybe_url).path
+ return None
+
+
async def send_file(
self: Client,
chat: ChatLike,
- path: Optional[Union[str, Path, File]] = None,
+ file: Union[str, Path, InFileLike, File],
*,
- url: Optional[str] = None,
- file: Optional[InFileLike] = None,
size: Optional[int] = None,
name: Optional[str] = None,
mime_type: Optional[str] = None,
@@ -161,61 +162,127 @@ async def send_file(
caption_markdown: Optional[str] = None,
caption_html: Optional[str] = None,
) -> Message:
- file_info = File.new(
- path,
- url=url,
- file=file,
- size=size,
- name=name,
- mime_type=mime_type,
- compress=compress,
- animated=animated,
- duration=duration,
- voice=voice,
- title=title,
- performer=performer,
- emoji=emoji,
- emoji_sticker=emoji_sticker,
- width=width,
- height=height,
- round=round,
- supports_streaming=supports_streaming,
- muted=muted,
- )
message, entities = parse_message(
text=caption, markdown=caption_markdown, html=caption_html, allow_empty=True
)
assert isinstance(message, str)
- peer = (await self._resolve_to_packed(chat))._to_input_peer()
+ # Re-send existing file.
+ if isinstance(file, File):
+ return await do_send_file(self, chat, file._input_media, message, entities)
- if file_info._input_media is None:
- if file_info._input_file is None:
- file_info._input_file = await upload(self, file_info)
- file_info._input_media = (
- types.InputMediaUploadedPhoto(
- spoiler=False,
- file=file_info._input_file,
- stickers=None,
- ttl_seconds=None,
+ # URLs are handled early as they can't use any other attributes either.
+ input_media: abcs.InputMedia
+ if (url_path := try_get_url_path(file)) is not None:
+ assert isinstance(file, str)
+ if compress:
+ if mime_type is None:
+ if name is None:
+ name = Path(url_path).name
+ mime_type, _ = mimetypes.guess_type(name, strict=False)
+ as_photo = mime_type and mime_type.startswith("image/")
+ else:
+ as_photo = False
+ if as_photo:
+ input_media = types.InputMediaPhotoExternal(
+ spoiler=False, url=file, ttl_seconds=None
)
- if file_info._photo
- else types.InputMediaUploadedDocument(
- nosound_video=file_info._muted,
- force_file=False,
- spoiler=False,
- file=file_info._input_file,
- thumb=None,
- mime_type=file_info._mime,
- attributes=file_info._attributes,
- stickers=None,
- ttl_seconds=None,
+ else:
+ input_media = types.InputMediaDocumentExternal(
+ spoiler=False, url=file, ttl_seconds=None
)
+ return await do_send_file(self, chat, input_media, message, entities)
+
+ # Paths are opened and closed by us. Anything else is *only* read, not closed.
+ if isinstance(file, (str, Path)):
+ path = Path(file) if isinstance(file, str) else file
+ if size is None:
+ size = path.stat().st_size
+ if name is None:
+ name = path.name
+ with path.open("rb") as fd:
+ input_file = await upload(self, fd, size, name)
+ else:
+ if size is None:
+ raise ValueError("size must be set when sending file-like objects")
+ if name is None:
+ raise ValueError("name must be set when sending file-like objects")
+ input_file = await upload(self, file, size, name)
+
+ # Mime is mandatory for documents, but we also use it to determine whether to send as photo.
+ if mime_type is None:
+ mime_type, _ = mimetypes.guess_type(name, strict=False)
+ if mime_type is None:
+ mime_type = "application/octet-stream"
+
+ as_photo = compress and mime_type.startswith("image/")
+ if as_photo:
+ input_media = types.InputMediaUploadedPhoto(
+ spoiler=False,
+ file=input_file,
+ stickers=None,
+ ttl_seconds=None,
)
- random_id = generate_random_id()
- return self._build_message_map(
- await self(
+ # Only bother to calculate attributes when sending documents.
+ else:
+ attributes: List[abcs.DocumentAttribute] = []
+ attributes.append(types.DocumentAttributeFilename(file_name=name))
+
+ if mime_type.startswith("image/"):
+ if width is not None and height is not None:
+ attributes.append(types.DocumentAttributeImageSize(w=width, h=height))
+ elif mime_type.startswith("audio/"):
+ if duration is not None:
+ attributes.append(
+ types.DocumentAttributeAudio(
+ voice=voice,
+ duration=int(math_round(duration)),
+ title=title,
+ performer=performer,
+ waveform=None,
+ )
+ )
+ elif mime_type.startswith("video/"):
+ if duration is not None and width is not None and height is not None:
+ attributes.append(
+ types.DocumentAttributeVideo(
+ round_message=round,
+ supports_streaming=supports_streaming,
+ nosound=muted,
+ duration=int(math_round(duration)),
+ w=width,
+ h=height,
+ preload_prefix_size=None,
+ )
+ )
+
+ input_media = types.InputMediaUploadedDocument(
+ nosound_video=muted,
+ force_file=False,
+ spoiler=False,
+ file=input_file,
+ thumb=None,
+ mime_type=mime_type,
+ attributes=attributes,
+ stickers=None,
+ ttl_seconds=None,
+ )
+
+ return await do_send_file(self, chat, input_media, message, entities)
+
+
+async def do_send_file(
+ client: Client,
+ chat: ChatLike,
+ input_media: abcs.InputMedia,
+ message: str,
+ entities: Optional[List[abcs.MessageEntity]],
+) -> Message:
+ peer = (await client._resolve_to_packed(chat))._to_input_peer()
+ random_id = generate_random_id()
+ return client._build_message_map(
+ await client(
functions.messages.send_media(
silent=False,
background=False,
@@ -224,7 +291,7 @@ async def send_file(
update_stickersets_order=False,
peer=peer,
reply_to=None,
- media=file_info._input_media,
+ media=input_media,
message=message,
random_id=random_id,
reply_markup=None,
@@ -239,67 +306,67 @@ async def send_file(
async def upload(
client: Client,
- file: File,
+ fd: InFileLike,
+ size: int,
+ name: str,
) -> abcs.InputFile:
file_id = generate_random_id()
uploaded = 0
part = 0
- total_parts = (file._size + MAX_CHUNK_SIZE - 1) // MAX_CHUNK_SIZE
+ total_parts = (size + MAX_CHUNK_SIZE - 1) // MAX_CHUNK_SIZE
buffer = bytearray()
to_store: Union[bytearray, bytes] = b""
hash_md5 = hashlib.md5()
- is_big = file._size > BIG_FILE_SIZE
+ is_big = size > BIG_FILE_SIZE
- fd = file._open()
- try:
- while uploaded != file._size:
- chunk = await fd.read(MAX_CHUNK_SIZE - len(buffer))
- if not chunk:
- raise ValueError("unexpected end-of-file")
+ while uploaded != size:
+ ret = fd.read(MAX_CHUNK_SIZE - len(buffer))
+ chunk = await ret if isawaitable(ret) else ret
+ assert isinstance(chunk, bytes)
+ if not chunk:
+ raise ValueError("unexpected end-of-file")
- if len(chunk) == MAX_CHUNK_SIZE or uploaded + len(chunk) == file._size:
- to_store = chunk
+ if len(chunk) == MAX_CHUNK_SIZE or uploaded + len(chunk) == size:
+ to_store = chunk
+ else:
+ buffer += chunk
+ if len(buffer) == MAX_CHUNK_SIZE:
+ to_store = buffer
else:
- buffer += chunk
- if len(buffer) == MAX_CHUNK_SIZE:
- to_store = buffer
- else:
- continue
+ continue
- if is_big:
- await client(
- functions.upload.save_big_file_part(
- file_id=file_id,
- file_part=part,
- file_total_parts=part,
- bytes=to_store,
- )
+ if is_big:
+ await client(
+ functions.upload.save_big_file_part(
+ file_id=file_id,
+ file_part=part,
+ file_total_parts=part,
+ bytes=to_store,
)
- else:
- await client(
- functions.upload.save_file_part(
- file_id=file_id, file_part=total_parts, bytes=to_store
- )
+ )
+ else:
+ await client(
+ functions.upload.save_file_part(
+ file_id=file_id, file_part=total_parts, bytes=to_store
)
- hash_md5.update(to_store)
+ )
+ hash_md5.update(to_store)
- buffer.clear()
- part += 1
- finally:
- fd.close()
+ buffer.clear()
+ part += 1
- if file._size > BIG_FILE_SIZE:
+ if is_big:
return types.InputFileBig(
id=file_id,
parts=total_parts,
- name=file._name,
+ name=name,
)
else:
return types.InputFile(
id=file_id,
parts=total_parts,
- name=file._name,
+ name=name,
md5_checksum=hash_md5.hexdigest(),
)
diff --git a/client/src/telethon/_impl/client/client/net.py b/client/src/telethon/_impl/client/client/net.py
index 86911deb..b22a9aee 100644
--- a/client/src/telethon/_impl/client/client/net.py
+++ b/client/src/telethon/_impl/client/client/net.py
@@ -1,10 +1,11 @@
from __future__ import annotations
import asyncio
+import itertools
import platform
import re
from dataclasses import dataclass, field
-from typing import TYPE_CHECKING, Optional, TypeVar
+from typing import TYPE_CHECKING, List, Optional, Tuple, TypeVar
from ....version import __version__
from ...mtproto import Full, RpcError
@@ -13,7 +14,7 @@ from ...mtsender import connect as connect_without_auth
from ...mtsender import connect_with_auth
from ...session import DataCenter, Session
from ...session import User as SessionUser
-from ...tl import LAYER, Request, functions
+from ...tl import LAYER, Request, functions, types
from ..errors import adapt_rpc
from .updates import dispatcher, process_socket_updates
@@ -41,13 +42,6 @@ def default_system_version() -> str:
@dataclass
class Config:
- """
- Configuration used by the :class:`telethon.Client`.
-
- See the parameters of :class:`~telethon.Client` for an explanation of the fields.
- """
-
- session: Session
api_id: int
api_hash: str
device_model: str = field(default_factory=default_device_model)
@@ -56,54 +50,33 @@ class Config:
system_lang_code: str = "en"
lang_code: str = "en"
catch_up: bool = False
- server_addr: Optional[str] = None
+ datacenter: Optional[DataCenter] = None
flood_sleep_threshold: Optional[int] = 60
update_queue_limit: Optional[int] = None
-# dc_id to IPv4 and port pair
-DC_ADDRESSES = [
- "0.0.0.0:0",
- "149.154.175.53:443",
- "149.154.167.51:443",
- "149.154.175.100:443",
- "149.154.167.92:443",
- "91.108.56.190:443",
+KNOWN_DC = [
+ DataCenter(id=1, addr="149.154.175.53:443", auth=None),
+ DataCenter(id=2, addr="149.154.167.51:443", auth=None),
+ DataCenter(id=3, addr="149.154.175.100:443", auth=None),
+ DataCenter(id=4, addr="149.154.167.92:443", auth=None),
+ DataCenter(id=5, addr="91.108.56.190:443", auth=None),
]
DEFAULT_DC = 2
-async def connect_sender(dc_id: int, config: Config) -> Sender:
+async def connect_sender(
+ config: Config, dc: DataCenter
+) -> Tuple[Sender, List[DataCenter]]:
transport = Full()
- if config.server_addr:
- addr = config.server_addr
+ if dc.auth:
+ sender = await connect_with_auth(transport, dc.id, dc.addr, dc.auth)
else:
- addr = DC_ADDRESSES[dc_id]
-
- auth_key: Optional[bytes] = None
- for dc in config.session.dcs:
- if dc.id == dc_id:
- if dc.auth:
- auth_key = dc.auth
- break
-
- if auth_key:
- sender = await connect_with_auth(transport, addr, auth_key)
- else:
- sender = await connect_without_auth(transport, addr)
- for dc in config.session.dcs:
- if dc.id == dc_id:
- dc.auth = sender.auth_key
- break
- else:
- config.session.dcs.append(
- DataCenter(id=dc_id, addr=addr, auth=sender.auth_key)
- )
+ sender = await connect_without_auth(transport, dc.id, dc.addr)
# TODO handle -404 (we had a previously-valid authkey, but server no longer knows about it)
- # TODO all up-to-date server addresses should be stored in the session for future initial connections
remote_config = await sender.invoke(
functions.invoke_with_layer(
layer=LAYER,
@@ -121,9 +94,29 @@ async def connect_sender(dc_id: int, config: Config) -> Sender:
),
)
)
- remote_config
- return sender
+ latest_dcs = []
+ append_current = True
+ for opt in types.Config.from_bytes(remote_config).dc_options:
+ assert isinstance(opt, types.DcOption)
+ latest_dcs.append(
+ DataCenter(
+ id=opt.id,
+ addr=opt.ip_address,
+ auth=sender.auth_key if sender.dc_id == opt.id else None,
+ )
+ )
+ if sender.dc_id == opt.id:
+ append_current = False
+
+ if append_current:
+ # Current config has no DC with current ID.
+ # Append it to preserve the authorization key.
+ latest_dcs.append(
+ DataCenter(id=sender.dc_id, addr=sender.addr, auth=sender.auth_key)
+ )
+
+ return sender, latest_dcs
async def connect(self: Client) -> None:
@@ -131,32 +124,44 @@ async def connect(self: Client) -> None:
return
if session := await self._storage.load():
- self._config.session = session
+ self._session = session
- if user := self._config.session.user:
- self._dc_id = user.dc
+ if dc := self._config.datacenter:
+ # Datacenter override, reusing the session's auth-key unless already present.
+ datacenter = (
+ dc
+ if dc.auth
+ else DataCenter(
+ id=dc.id,
+ addr=dc.addr,
+ auth=next(
+ (d.auth for d in self._session.dcs if d.id == dc.id and d.auth),
+ None,
+ ),
+ )
+ )
else:
- for dc in self._config.session.dcs:
- if dc.auth:
- self._dc_id = dc.id
- break
+ # Reuse the session's datacenter, falling back to defaults if not found.
+ datacenter = datacenter_for_id(
+ self, self._session.user.dc if self._session.user else DEFAULT_DC
+ )
- self._sender = await connect_sender(self._dc_id, self._config)
+ self._sender, self._session.dcs = await connect_sender(self._config, datacenter)
- if self._message_box.is_empty() and self._config.session.user:
+ if self._message_box.is_empty() and self._session.user:
try:
await self(functions.updates.get_state())
except RpcError as e:
if e.code == 401:
- self._config.session.user = None
+ self._session.user = None
except Exception as e:
pass
else:
- if not self._config.session.user:
+ if not self._session.user:
me = await self.get_me()
assert me is not None
- self._config.session.user = SessionUser(
- id=me.id, dc=self._dc_id, bot=me.bot, username=me.username
+ self._session.user = SessionUser(
+ id=me.id, dc=self._sender.dc_id, bot=me.bot, username=me.username
)
packed = me.pack()
assert packed is not None
@@ -165,6 +170,17 @@ async def connect(self: Client) -> None:
self._dispatcher = asyncio.create_task(dispatcher(self))
+def datacenter_for_id(client: Client, dc_id: int) -> DataCenter:
+ try:
+ return next(
+ dc
+ for dc in itertools.chain(client._session.dcs, KNOWN_DC)
+ if dc.id == dc_id
+ )
+ except StopIteration:
+ raise ValueError(f"no datacenter found for id: {dc_id}") from None
+
+
async def disconnect(self: Client) -> None:
if not self._sender:
return
@@ -173,8 +189,6 @@ async def disconnect(self: Client) -> None:
self._dispatcher.cancel()
try:
await self._dispatcher
- except asyncio.CancelledError:
- pass
except Exception:
pass # TODO log
finally:
@@ -187,8 +201,8 @@ async def disconnect(self: Client) -> None:
finally:
self._sender = None
- self._config.session.state = self._message_box.session_state()
- await self._storage.save(self._config.session)
+ self._session.state = self._message_box.session_state()
+ await self._storage.save(self._session)
async def invoke_request(
diff --git a/client/src/telethon/_impl/client/client/updates.py b/client/src/telethon/_impl/client/client/updates.py
index db44170d..0a506374 100644
--- a/client/src/telethon/_impl/client/client/updates.py
+++ b/client/src/telethon/_impl/client/client/updates.py
@@ -123,14 +123,24 @@ def extend_update_queue(
async def dispatcher(client: Client) -> None:
+ loop = asyncio.get_running_loop()
while client.connected:
try:
await dispatch_next(client)
except asyncio.CancelledError:
- raise
- except Exception:
- # TODO proper logger
- logging.exception("Unhandled exception in event handler")
+ return
+ except Exception as e:
+ if isinstance(e, RuntimeError) and loop.is_closed:
+ # User probably forgot to call disconnect.
+ logging.warning(
+ "client was not closed cleanly, make sure to call client.disconnect()! %s",
+ e,
+ )
+ return
+ else:
+ # TODO proper logger
+ logging.exception("Unhandled exception in event handler")
+ raise
async def dispatch_next(client: Client) -> None:
diff --git a/client/src/telethon/_impl/client/client/users.py b/client/src/telethon/_impl/client/client/users.py
index 3b23e4a4..33c64733 100644
--- a/client/src/telethon/_impl/client/client/users.py
+++ b/client/src/telethon/_impl/client/client/users.py
@@ -76,10 +76,10 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
if isinstance(chat, types.InputPeerEmpty):
raise ValueError("Cannot resolve chat")
elif isinstance(chat, types.InputPeerSelf):
- if not self._config.session.user:
+ if not self._session.user:
raise ValueError("Cannot resolve chat")
return PackedChat(
- ty=PackedType.BOT if self._config.session.user.bot else PackedType.USER,
+ ty=PackedType.BOT if self._session.user.bot else PackedType.USER,
id=self._chat_hashes.self_id,
access_hash=0, # TODO get hash
)
@@ -112,7 +112,7 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
if chat.startswith("+"):
resolved = await resolve_phone(self, chat)
elif chat == "me":
- if me := self._config.session.user:
+ if me := self._session.user:
return PackedChat(
ty=PackedType.BOT if me.bot else PackedType.USER,
id=me.id,
diff --git a/client/src/telethon/_impl/client/events/filters/messages.py b/client/src/telethon/_impl/client/events/filters/messages.py
index 288b36b7..c5a3b398 100644
--- a/client/src/telethon/_impl/client/events/filters/messages.py
+++ b/client/src/telethon/_impl/client/events/filters/messages.py
@@ -72,7 +72,7 @@ class Command:
self._username = ""
client: Optional[Client]
if (client := getattr(event, "_client", None)) is not None:
- user = client._config.session.user
+ user = client._session.user
if user and user.username:
self._username = user.username
diff --git a/client/src/telethon/_impl/client/types/__init__.py b/client/src/telethon/_impl/client/types/__init__.py
index d1cd7826..b1f7452d 100644
--- a/client/src/telethon/_impl/client/types/__init__.py
+++ b/client/src/telethon/_impl/client/types/__init__.py
@@ -2,12 +2,13 @@ from .async_list import AsyncList
from .chat import Channel, Chat, ChatLike, Group, RestrictionReason, User
from .dialog import Dialog
from .draft import Draft
-from .file import File, InFileLike, InWrapper, OutFileLike, OutWrapper
+from .file import File, InFileLike, OutFileLike, OutWrapper
from .login_token import LoginToken
from .message import Message
from .meta import NoPublicConstructor
from .participant import Participant
from .password_token import PasswordToken
+from .inline_result import InlineResult
from .recent_action import RecentAction
__all__ = [
@@ -22,9 +23,9 @@ __all__ = [
"Draft",
"File",
"InFileLike",
- "InWrapper",
"OutFileLike",
"OutWrapper",
+ "InlineResult",
"LoginToken",
"Message",
"NoPublicConstructor",
diff --git a/client/src/telethon/_impl/client/types/file.py b/client/src/telethon/_impl/client/types/file.py
index 1e38a2fc..95baa911 100644
--- a/client/src/telethon/_impl/client/types/file.py
+++ b/client/src/telethon/_impl/client/types/file.py
@@ -44,7 +44,7 @@ stripped_size_header = bytes.fromhex(
stripped_size_footer = bytes.fromhex("FFD9")
-def expand_stripped_size(data: bytes) -> bytearray:
+def expand_stripped_size(data: bytes) -> bytes:
header = bytearray(stripped_size_header)
header[164] = data[1]
header[166] = data[2]
@@ -77,7 +77,12 @@ class InFileLike(Protocol):
"""
def read(self, n: int) -> Union[bytes, Coroutine[Any, Any, bytes]]:
- pass
+ """
+ Read from the file or buffer.
+
+ :param n:
+ Maximum amount of bytes that should be returned.
+ """
class OutFileLike(Protocol):
@@ -87,33 +92,12 @@ class OutFileLike(Protocol):
"""
def write(self, data: bytes) -> Union[Any, Coroutine[Any, Any, Any]]:
- pass
+ """
+ Write all the data into the file or buffer.
-
-class InWrapper:
- __slots__ = ("_fd", "_owned")
-
- def __init__(self, file: Union[str, Path, InFileLike]):
- if isinstance(file, str):
- file = Path(file)
-
- if isinstance(file, Path):
- self._fd: Union[InFileLike, BufferedReader] = file.open("rb")
- self._owned = True
- else:
- self._fd = file
- self._owned = False
-
- async def read(self, n: int) -> bytes:
- ret = self._fd.read(n)
- chunk = await ret if isawaitable(ret) else ret
- assert isinstance(chunk, bytes)
- return chunk
-
- def close(self) -> None:
- if self._owned:
- assert hasattr(self._fd, "close")
- self._fd.close()
+ :param data:
+ Data that must be written to the buffer entirely.
+ """
class OutWrapper:
@@ -143,30 +127,24 @@ class OutWrapper:
class File(metaclass=NoPublicConstructor):
"""
- File information of uploaded media.
-
- It is used both when sending files or accessing media in a `Message`.
+ File information of media sent to Telegram that can be downloaded.
"""
def __init__(
self,
*,
- path: Optional[Path],
- file: Optional[InFileLike],
attributes: List[abcs.DocumentAttribute],
size: int,
name: str,
mime: str,
photo: bool,
muted: bool,
- input_media: Optional[abcs.InputMedia],
+ input_media: abcs.InputMedia,
thumb: Optional[abcs.PhotoSize],
thumbs: Optional[List[abcs.PhotoSize]],
raw: Optional[Union[abcs.MessageMedia, abcs.Photo, abcs.Document]],
client: Optional[Client],
):
- self._path = path
- self._file = file
self._attributes = attributes
self._size = size
self._name = name
@@ -226,8 +204,6 @@ class File(metaclass=NoPublicConstructor):
) -> Optional[Self]:
if isinstance(raw, types.Document):
return cls._create(
- path=None,
- file=None,
attributes=raw.attributes,
size=raw.size,
name=next(
@@ -279,8 +255,6 @@ class File(metaclass=NoPublicConstructor):
if isinstance(raw, types.Photo):
largest_thumb = max(raw.sizes, key=photo_size_byte_count)
return cls._create(
- path=None,
- file=None,
attributes=[],
size=photo_size_byte_count(largest_thumb),
name="",
@@ -304,165 +278,29 @@ class File(metaclass=NoPublicConstructor):
return None
- @classmethod
- def new(
- cls,
- path: Optional[Union[str, Path, Self]] = None,
- *,
- url: Optional[str] = None,
- file: Optional[InFileLike] = None,
- size: Optional[int] = None,
- name: Optional[str] = None,
- mime_type: Optional[str] = None,
- compress: bool = False,
- animated: bool = False,
- duration: Optional[float] = None,
- voice: bool = False,
- title: Optional[str] = None,
- performer: Optional[str] = None,
- emoji: Optional[str] = None,
- emoji_sticker: Optional[str] = None,
- width: Optional[int] = None,
- height: Optional[int] = None,
- round: bool = False,
- supports_streaming: bool = False,
- muted: bool = False,
- ) -> Self:
+ @property
+ def name(self) -> Optional[str]:
"""
- Create file information that can later be sent as media.
-
- If the path is a `File`, the rest of parameters are ignored, and
- this existing instance is returned instead (the method is a no-op).
-
- Only one of path, url or file must be specified.
-
- If a local file path is not given, size and name must be specified.
-
- The mime_type will be inferred from the name if it is omitted.
-
- The rest of parameters are only used depending on the mime_type:
-
- * For image/:
- * width (required), in pixels, of the media.
- * height (required), in pixels, of the media.
- * For audio/:
- * duration (required), in seconds, of the media. This will be rounded.
- * voice, if it's a live recording.
- * title, of the song.
- * performer, with the name of the artist.
- * For video/:
- * duration (required), in seconds, of the media. This will be rounded.
- * width (required), in pixels, of the media.
- * height (required), in pixels, of the media.
- * round, if it should be displayed as a round video.
- * supports_streaming, if clients are able to stream the video.
- * muted, if the sound from the video is or should be missing.
- * For sticker:
- * animated, if it's not a static image.
- * emoji, as the alternative text for the sticker.
- * stickerset, to which the sticker belongs.
-
- If any of the required fields are missing, the attribute will not be sent.
+ The file name, if known.
"""
- if isinstance(path, cls):
- return path
- assert not isinstance(path, File)
+ for attr in self._attributes:
+ if isinstance(attr, types.DocumentAttributeFilename):
+ return attr.file_name
- attributes: List[abcs.DocumentAttribute] = []
-
- if sum((path is not None, url is not None, file is not None)) != 1:
- raise ValueError("must specify exactly one of path, markdown or html")
-
- if path is not None:
- size = os.path.getsize(path)
- name = os.path.basename(path)
-
- if size is None:
- raise ValueError("must specify size")
- if name is None:
- raise ValueError("must specify name")
-
- if mime_type is None:
- mime_type, _ = guess_type(name, strict=False)
- if mime_type is None:
- raise ValueError("must specify mime_type")
-
- if sum((path is not None, url is not None, file is not None)) != 1:
- raise ValueError("must specify exactly one of path, markdown or html")
-
- attributes.append(types.DocumentAttributeFilename(file_name=name))
-
- if mime_type.startswith("image/"):
- if width is not None and height is not None:
- attributes.append(types.DocumentAttributeImageSize(w=width, h=height))
- elif mime_type.startswith("audio/"):
- if duration is not None:
- attributes.append(
- types.DocumentAttributeAudio(
- voice=voice,
- duration=int(math_round(duration)),
- title=title,
- performer=performer,
- waveform=None,
- )
- )
- elif mime_type.startswith("video/"):
- if duration is not None and width is not None and height is not None:
- attributes.append(
- types.DocumentAttributeVideo(
- round_message=round,
- supports_streaming=supports_streaming,
- nosound=muted,
- duration=int(math_round(duration)),
- w=width,
- h=height,
- preload_prefix_size=None,
- )
- )
-
- photo = compress and mime_type.startswith("image/")
-
- input_media: Optional[abcs.InputMedia]
- if url is not None:
- if photo:
- input_media = types.InputMediaPhotoExternal(
- spoiler=False, url=url, ttl_seconds=None
- )
- else:
- input_media = types.InputMediaDocumentExternal(
- spoiler=False, url=url, ttl_seconds=None
- )
- else:
- input_media = None
-
- return cls._create(
- path=Path(path) if path is not None else None,
- file=file,
- attributes=attributes,
- size=size,
- name=name,
- mime=mime_type,
- photo=photo,
- muted=muted,
- input_media=input_media,
- thumb=None,
- thumbs=None,
- raw=None,
- client=None,
- )
+ return None
@property
def ext(self) -> str:
"""
The file extension, including the leading dot ``.``.
- If the file does not represent and local file, the mimetype is used in :meth:`mimetypes.guess_extension`.
+ If the name is not known, the mime-type is used in :meth:`mimetypes.guess_extension`.
- If no extension is known for the mimetype, the empty string will be returned.
+ If no extension is known for the mime-type, the empty string will be returned.
This makes it safe to always append this property to a file name.
"""
- if self._path:
- return self._path.suffix
+ if name := self._name:
+ return Path(name).suffix
else:
return mimetypes.guess_extension(self._mime) or ""
@@ -477,8 +315,6 @@ class File(metaclass=NoPublicConstructor):
"""
return [
File._create(
- path=None,
- file=None,
attributes=[],
size=photo_size_byte_count(t),
name="",
@@ -530,22 +366,13 @@ class File(metaclass=NoPublicConstructor):
"""
Alias for :meth:`telethon.Client.download`.
- The file must have been obtained from Telegram to be downloadable.
- This means you cannot create local files, or files with an URL, and download them.
-
- See the documentation of :meth:`~telethon.Client.download` for an explanation of the parameters.
+ :param file: See :meth:`~telethon.Client.download`.
"""
if not self._client:
raise ValueError("only files from Telegram can be downloaded")
await self._client.download(self, file)
- def _open(self) -> InWrapper:
- file = self._file or self._path
- if file is None:
- raise TypeError(f"cannot use file for uploading: {self}")
- return InWrapper(file)
-
def _input_location(self) -> abcs.InputFileLocation:
thumb_types = (
types.PhotoSizeEmpty,
diff --git a/client/src/telethon/_impl/client/types/inline_result.py b/client/src/telethon/_impl/client/types/inline_result.py
new file mode 100644
index 00000000..e5d908e0
--- /dev/null
+++ b/client/src/telethon/_impl/client/types/inline_result.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Optional, Union
+
+from ..utils import generate_random_id
+from ...tl import abcs, types, functions
+from .chat import ChatLike
+from .meta import NoPublicConstructor
+from .message import Message
+
+if TYPE_CHECKING:
+ from ..client import Client
+
+
+class InlineResult(metaclass=NoPublicConstructor):
+ def __init__(
+ self,
+ client: Client,
+ results: types.messages.BotResults,
+ result: Union[types.BotInlineMediaResult, types.BotInlineResult],
+ default_peer: abcs.InputPeer,
+ ):
+ self._client = client
+ self._raw_results = results
+ self._raw = result
+ self._default_peer = default_peer
+
+ @property
+ def type(self) -> str:
+ return self._raw.type
+
+ @property
+ def title(self) -> str:
+ return self._raw.title or ""
+
+ @property
+ def description(self) -> Optional[str]:
+ return self._raw.description
+
+ async def send(
+ self,
+ chat: Optional[ChatLike] = None,
+ ) -> Message:
+ """
+ Send the inline result to the desired chat.
+
+ :param chat:
+ The chat where the inline result should be sent to.
+
+ This can be omitted if a chat was previously specified in the :meth:`~Client.inline_query`.
+
+ :return: The sent message.
+ """
+ if chat is None and isinstance(self._default_peer, types.InputPeerEmpty):
+ raise ValueError("no target chat was specified")
+
+ if chat is not None:
+ peer = (await self._client._resolve_to_packed(chat))._to_input_peer()
+ else:
+ peer = self._default_peer
+
+ random_id = generate_random_id()
+ return self._client._build_message_map(
+ await self._client(
+ functions.messages.send_inline_bot_result(
+ silent=False,
+ background=False,
+ clear_draft=False,
+ hide_via=False,
+ peer=peer,
+ reply_to=None,
+ random_id=random_id,
+ query_id=self._raw_results.query_id,
+ id=self._raw.id,
+ schedule_date=None,
+ send_as=None,
+ )
+ ),
+ peer,
+ ).with_random_id(random_id)
diff --git a/client/src/telethon/_impl/client/types/message.py b/client/src/telethon/_impl/client/types/message.py
index bf60d3d0..299f5df6 100644
--- a/client/src/telethon/_impl/client/types/message.py
+++ b/client/src/telethon/_impl/client/types/message.py
@@ -186,7 +186,10 @@ class Message(metaclass=NoPublicConstructor):
"""
Alias for :meth:`telethon.Client.send_message`.
- See the documentation of :meth:`~telethon.Client.send_message` for an explanation of the parameters.
+ :param text: See :ref:`formatting`.
+ :param markdown: See :ref:`formatting`.
+ :param html: See :ref:`formatting`.
+ :param link_preview: See :meth:`~telethon.Client.send_message`.
"""
return await self._client.send_message(
self.chat, text, markdown=markdown, html=html, link_preview=link_preview
@@ -203,7 +206,10 @@ class Message(metaclass=NoPublicConstructor):
"""
Alias for :meth:`telethon.Client.send_message` with the ``reply_to`` parameter set to this message.
- See the documentation of :meth:`~telethon.Client.send_message` for an explanation of the parameters.
+ :param text: See :ref:`formatting`.
+ :param markdown: See :ref:`formatting`.
+ :param html: See :ref:`formatting`.
+ :param link_preview: See :meth:`~telethon.Client.send_message`.
"""
return await self._client.send_message(
self.chat,
@@ -218,7 +224,7 @@ class Message(metaclass=NoPublicConstructor):
"""
Alias for :meth:`telethon.Client.delete_messages`.
- See the documentation of :meth:`~telethon.Client.delete_messages` for an explanation of the parameters.
+ :param revoke: See :meth:`~telethon.Client.delete_messages`.
"""
await self._client.delete_messages(self.chat, [self.id], revoke=revoke)
@@ -232,7 +238,10 @@ class Message(metaclass=NoPublicConstructor):
"""
Alias for :meth:`telethon.Client.edit_message`.
- See the documentation of :meth:`~telethon.Client.edit_message` for an explanation of the parameters.
+ :param text: See :ref:`formatting`.
+ :param markdown: See :ref:`formatting`.
+ :param html: See :ref:`formatting`.
+ :param link_preview: See :meth:`~telethon.Client.send_message`.
"""
return await self._client.edit_message(
self.chat,
@@ -247,17 +256,23 @@ class Message(metaclass=NoPublicConstructor):
"""
Alias for :meth:`telethon.Client.forward_messages`.
- See the documentation of :meth:`~telethon.Client.forward_messages` for an explanation of the parameters.
+ :param target: See :meth:`~telethon.Client.forward_messages`.
"""
return (await self._client.forward_messages(target, [self.id], self.chat))[0]
async def mark_read(self) -> None:
pass
- async def pin(self, *, notify: bool = False, pm_oneside: bool = False) -> None:
+ async def pin(self) -> None:
+ """
+ Alias for :meth:`telethon.Client.pin_message`.
+ """
pass
async def unpin(self) -> None:
+ """
+ Alias for :meth:`telethon.Client.unpin_message`.
+ """
pass
# ---
diff --git a/client/src/telethon/_impl/mtsender/sender.py b/client/src/telethon/_impl/mtsender/sender.py
index 2b0d3857..ba8cc76f 100644
--- a/client/src/telethon/_impl/mtsender/sender.py
+++ b/client/src/telethon/_impl/mtsender/sender.py
@@ -76,6 +76,8 @@ class Request(Generic[Return]):
@dataclass
class Sender:
+ dc_id: int
+ addr: str
_reader: StreamReader
_writer: StreamWriter
_transport: Transport
@@ -88,10 +90,14 @@ class Sender:
_write_drain_pending: bool
@classmethod
- async def connect(cls, transport: Transport, mtp: Mtp, addr: str) -> Self:
+ async def connect(
+ cls, transport: Transport, mtp: Mtp, dc_id: int, addr: str
+ ) -> Self:
reader, writer = await asyncio.open_connection(*addr.split(":"))
return cls(
+ dc_id=dc_id,
+ addr=addr,
_reader=reader,
_writer=writer,
_transport=transport,
@@ -271,8 +277,8 @@ class Sender:
return None
-async def connect(transport: Transport, addr: str) -> Sender:
- sender = await Sender.connect(transport, Plain(), addr)
+async def connect(transport: Transport, dc_id: int, addr: str) -> Sender:
+ sender = await Sender.connect(transport, Plain(), dc_id, addr)
return await generate_auth_key(sender)
@@ -289,6 +295,8 @@ async def generate_auth_key(sender: Sender) -> Sender:
first_salt = finished.first_salt
return Sender(
+ dc_id=sender.dc_id,
+ addr=sender.addr,
_reader=sender._reader,
_writer=sender._writer,
_transport=sender._transport,
@@ -304,9 +312,10 @@ async def generate_auth_key(sender: Sender) -> Sender:
async def connect_with_auth(
transport: Transport,
+ dc_id: int,
addr: str,
auth_key: bytes,
) -> Sender:
return await Sender.connect(
- transport, Encrypted(AuthKey.from_bytes(auth_key)), addr
+ transport, Encrypted(AuthKey.from_bytes(auth_key)), dc_id, addr
)
diff --git a/client/src/telethon/_impl/session/chat/packed.py b/client/src/telethon/_impl/session/chat/packed.py
index 3e6d2738..da31c139 100644
--- a/client/src/telethon/_impl/session/chat/packed.py
+++ b/client/src/telethon/_impl/session/chat/packed.py
@@ -67,6 +67,9 @@ class PackedChat:
"""
Convenience method to convert hexadecimal numbers into bytes then passed to :meth:`from_bytes`:
+ :param hex:
+ Hexadecimal numbers to convert from.
+
.. code-block::
assert PackedChat.from_hex(packed.hex) == packed
diff --git a/client/src/telethon/_impl/session/session.py b/client/src/telethon/_impl/session/session.py
index a4091a69..d97599f3 100644
--- a/client/src/telethon/_impl/session/session.py
+++ b/client/src/telethon/_impl/session/session.py
@@ -6,62 +6,71 @@ class DataCenter:
"""
Data-center information.
- :var id: The DC identifier.
- :var addr: The server address of the DC, in ``'ip:port'`` format.
- :var auth: Authentication key to encrypt communication with.
+ :param id: See below.
+ :param addr: See below.
+ :param auth: See below.
"""
__slots__ = ("id", "addr", "auth")
def __init__(self, *, id: int, addr: str, auth: Optional[bytes]) -> None:
self.id = id
+ "The DC identifier."
self.addr = addr
+ "The server address of the DC, in ``'ip:port'`` format."
self.auth = auth
+ "Authentication key to encrypt communication with."
class User:
"""
Information about the logged-in user.
- :var id: User identifier.
- :var dc: Data-center identifier of the user's "home" DC.
- :var bot: :data:`True` if the user is from a bot account.
- :var username: User's primary username.
+ :param id: See below.
+ :param dc: See below.
+ :param bot: See below.
+ :param username: See below.
"""
__slots__ = ("id", "dc", "bot", "username")
def __init__(self, *, id: int, dc: int, bot: bool, username: Optional[str]) -> None:
self.id = id
+ "User identifier."
self.dc = dc
+ 'Data-center identifier of the user\'s "home" DC.'
self.bot = bot
+ ":data:`True` if the user is from a bot account."
self.username = username
+ "User's primary username."
class ChannelState:
"""
Update state for a channel.
- :var id: The channel identifier.
- :var pts: The channel's partial sequence number.
+ :param id: See below.
+ :param pts: See below.
"""
__slots__ = ("id", "pts")
def __init__(self, *, id: int, pts: int) -> None:
self.id = id
+ "The channel identifier."
self.pts = pts
+ "The channel's partial sequence number."
class UpdateState:
"""
Update state for an account.
- :var pts: The primary partial sequence number.
- :var qts: The secondary partial sequence number.
- :var date: Date of the latest update sequence.
- :var seq: The sequence number.
- :var channels: Update state for channels.
+ :param pts: See below.
+ :param qts: See below.
+ :param date: See below.
+ :param seq: See below.
+ :param channels: See below.
"""
__slots__ = (
@@ -82,10 +91,15 @@ class UpdateState:
channels: List[ChannelState],
) -> None:
self.pts = pts
+ "The primary partial sequence number."
self.qts = qts
+ "The secondary partial sequence number."
self.date = date
+ "Date of the latest update sequence."
self.seq = seq
+ "The sequence number."
self.channels = channels
+ "Update state for channels."
class Session:
@@ -102,9 +116,18 @@ class Session:
If you think the session has been compromised, immediately terminate all
sessions through an official Telegram client to revoke the authorization.
+
+ :param dcs: See below.
+ :param user: See below.
+ :param state: See below.
"""
VERSION = 1
+ """
+ Current version.
+
+ Will be incremented if new fields are added.
+ """
__slots__ = ("dcs", "user", "state")
@@ -116,81 +139,8 @@ class Session:
state: Optional[UpdateState] = None,
):
self.dcs = dcs or []
+ "List of known data-centers."
self.user = user
+ "Information about the logged-in user."
self.state = state
-
- def to_dict(self) -> Dict[str, Any]:
- return {
- "v": self.VERSION,
- "dcs": [
- {
- "id": dc.id,
- "addr": dc.addr,
- "auth": base64.b64encode(dc.auth).decode("ascii")
- if dc.auth
- else None,
- }
- for dc in self.dcs
- ],
- "user": {
- "id": self.user.id,
- "dc": self.user.dc,
- "bot": self.user.bot,
- "username": self.user.username,
- }
- if self.user
- else None,
- "state": {
- "pts": self.state.pts,
- "qts": self.state.qts,
- "date": self.state.date,
- "seq": self.state.seq,
- "channels": [
- {"id": channel.id, "pts": channel.pts}
- for channel in self.state.channels
- ],
- }
- if self.state
- else None,
- }
-
- @classmethod
- def from_dict(cls, dict: Dict[str, Any]) -> Self:
- version = dict["v"]
- if version != cls.VERSION:
- raise ValueError(
- f"cannot parse session format version {version} (expected {cls.VERSION})"
- )
-
- return cls(
- dcs=[
- DataCenter(
- id=dc["id"],
- addr=dc["addr"],
- auth=base64.b64decode(dc["auth"])
- if dc["auth"] is not None
- else None,
- )
- for dc in dict["dcs"]
- ],
- user=User(
- id=dict["user"]["id"],
- dc=dict["user"]["dc"],
- bot=dict["user"]["bot"],
- username=dict["user"]["username"],
- )
- if dict["user"]
- else None,
- state=UpdateState(
- pts=dict["state"]["pts"],
- qts=dict["state"]["qts"],
- date=dict["state"]["date"],
- seq=dict["state"]["seq"],
- channels=[
- ChannelState(id=channel["id"], pts=channel["pts"])
- for channel in dict["state"]["channels"]
- ],
- )
- if dict["state"]
- else None,
- )
+ "Update state."
diff --git a/client/src/telethon/_impl/session/storage/storage.py b/client/src/telethon/_impl/session/storage/storage.py
index 0ab1a3c4..fcf56f47 100644
--- a/client/src/telethon/_impl/session/storage/storage.py
+++ b/client/src/telethon/_impl/session/storage/storage.py
@@ -12,17 +12,22 @@ class Storage(abc.ABC):
@abc.abstractmethod
async def load(self) -> Optional[Session]:
"""
- Load the `Session` instance, if any.
+ Load the :class:`Session` instance, if any.
This method is called by the library prior to `connect`.
+
+ :return: The previously-saved session.
"""
@abc.abstractmethod
async def save(self, session: Session) -> None:
"""
- Save the `Session` instance to persistent storage.
+ Save the :class:`Session` instance to persistent storage.
This method is called by the library post `disconnect`.
+
+ :param session:
+ The session information that should be persisted.
"""
@abc.abstractmethod
diff --git a/client/src/telethon/events/__init__.py b/client/src/telethon/events/__init__.py
index b5acf921..220eabb2 100644
--- a/client/src/telethon/events/__init__.py
+++ b/client/src/telethon/events/__init__.py
@@ -1,3 +1,8 @@
+"""
+Classes related to the different event types that wrap incoming Telegram updates.
+
+See the :doc:`/concepts/updates` concept for more details.
+"""
from .._impl.client.events import (
CallbackQuery,
Event,
diff --git a/client/src/telethon/session.py b/client/src/telethon/session.py
index eda49dd2..a87159ae 100644
--- a/client/src/telethon/session.py
+++ b/client/src/telethon/session.py
@@ -1,3 +1,8 @@
+"""
+Classes related to session data and session storages.
+
+See the :doc:`/concepts/sessions` concept for more details.
+"""
from ._impl.session import (
ChannelState,
DataCenter,
diff --git a/client/src/telethon/types.py b/client/src/telethon/types.py
index 6612ed84..1fea0edc 100644
--- a/client/src/telethon/types.py
+++ b/client/src/telethon/types.py
@@ -1,3 +1,6 @@
+"""
+Classes for the various objects the library returns.
+"""
from ._impl.client.client import Config, InlineResult
from ._impl.client.types import (
AsyncList,
diff --git a/client/tests/client_test.py b/client/tests/client_test.py
index 4401a65b..3d08ce4d 100644
--- a/client/tests/client_test.py
+++ b/client/tests/client_test.py
@@ -2,7 +2,7 @@ import os
import random
from pytest import mark
-from telethon import Client, Config, Session
+from telethon import Client
from telethon import _tl as tl
@@ -22,3 +22,5 @@ async def test_ping_pong() -> None:
pong = await client(tl.mtproto.functions.ping(ping_id=ping_id))
assert isinstance(pong, tl.mtproto.types.Pong)
assert pong.ping_id == ping_id
+
+ await client.disconnect()
diff --git a/client/tests/mtsender_test.py b/client/tests/mtsender_test.py
index 78279935..49f5599a 100644
--- a/client/tests/mtsender_test.py
+++ b/client/tests/mtsender_test.py
@@ -4,11 +4,10 @@ import logging
from pytest import LogCaptureFixture, mark
from telethon._impl.mtproto import Full
from telethon._impl.mtsender import connect
+from telethon._impl.session import DataCenter
from telethon._impl.tl import LAYER, abcs, functions, types
-TELEGRAM_TEST_DC_2 = "149.154.167.40:443"
-
-TELEGRAM_DEFAULT_TEST_DC = TELEGRAM_TEST_DC_2
+TELEGRAM_TEST_DC = 2, "149.154.167.40:443"
TEST_TIMEOUT = 10000
@@ -22,9 +21,7 @@ async def test_invoke_encrypted_method(caplog: LogCaptureFixture) -> None:
def timeout() -> float:
return deadline - asyncio.get_running_loop().time()
- sender = await asyncio.wait_for(
- connect(Full(), TELEGRAM_DEFAULT_TEST_DC), timeout()
- )
+ sender = await asyncio.wait_for(connect(Full(), *TELEGRAM_TEST_DC), timeout())
rx = sender.enqueue(
functions.invoke_with_layer(