mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2024-11-22 09:26:37 +03:00
Continue documentation and reducing public API
This commit is contained in:
parent
25a2b53d3f
commit
7fabf7da0a
|
@ -1,22 +1,3 @@
|
||||||
Code generation:
|
# Developing
|
||||||
|
|
||||||
```sh
|
See [Contributing](./client/doc/developing/contributing.rst).
|
||||||
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]`).
|
|
||||||
|
|
|
@ -352,7 +352,8 @@ For the most part, it's a 1-to-1 translation and the result is idiomatic Teletho
|
||||||
|
|
||||||
|
|
||||||
Migrating from aiogram
|
Migrating from aiogram
|
||||||
``````````````````````
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Using one of the examples from their v3 documentation with logging and comments removed:
|
Using one of the examples from their v3 documentation with logging and comments removed:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
|
@ -55,7 +55,7 @@ To check for a concrete type, you can use :func:`isinstance`:
|
||||||
if isinstance(invite, tl.types.ChatInviteAlready):
|
if isinstance(invite, tl.types.ChatInviteAlready):
|
||||||
print(invite.chat)
|
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:
|
Instead, there are multiple alternatives:
|
||||||
|
|
||||||
* Use Telethon's separate site to search in the `Telethon Raw API <https://tl.telethon.dev/>`_.
|
* Use Telethon's separate site to search in the `Telethon Raw API <https://tl.telethon.dev/>`_.
|
||||||
|
|
|
@ -66,3 +66,5 @@ Glossary
|
||||||
Type Language
|
Type Language
|
||||||
File format used by Telegram to define all the types and requests available in a :term:`layer`.
|
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 <https://core.telegram.org/mtproto/TL>`_.
|
Telegram's site has an `Overview of the TL language <https://core.telegram.org/mtproto/TL>`_.
|
||||||
|
|
||||||
|
.. seealso:: :ref:`Type Language brief <tl-brief>`.
|
||||||
|
|
|
@ -15,6 +15,8 @@ Messages
|
||||||
Messages are at the heart of a messaging platform.
|
Messages are at the heart of a messaging platform.
|
||||||
In Telethon, you will be using the :class:`~types.Message` class to interact with them.
|
In Telethon, you will be using the :class:`~types.Message` class to interact with them.
|
||||||
|
|
||||||
|
.. _formatting:
|
||||||
|
|
||||||
Formatting messages
|
Formatting messages
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ extensions = [
|
||||||
"sphinx.ext.autodoc",
|
"sphinx.ext.autodoc",
|
||||||
"sphinx.ext.intersphinx",
|
"sphinx.ext.intersphinx",
|
||||||
"sphinx.ext.graphviz",
|
"sphinx.ext.graphviz",
|
||||||
|
"sphinx.ext.coverage",
|
||||||
"roles.tl",
|
"roles.tl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -31,7 +32,9 @@ tl_ref_url = "https://tl.telethon.dev"
|
||||||
autodoc_default_options = {
|
autodoc_default_options = {
|
||||||
"members": True,
|
"members": True,
|
||||||
"undoc-members": True,
|
"undoc-members": True,
|
||||||
|
"show-inheritance": True,
|
||||||
}
|
}
|
||||||
|
autodoc_typehints = "description"
|
||||||
modindex_common_prefix = ["telethon."]
|
modindex_common_prefix = ["telethon."]
|
||||||
graphviz_output_format = "svg"
|
graphviz_output_format = "svg"
|
||||||
|
|
||||||
|
|
|
@ -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 <http://www.diveintopython3.net/>`_.
|
|
||||||
|
|
||||||
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.
|
|
|
@ -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 <http://www.diveintopython3.net/>`_.
|
||||||
|
|
||||||
|
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
|
.. currentmodule:: telethon
|
||||||
|
|
||||||
|
@ -7,29 +52,61 @@ The repository contains several folders, each with their own "package".
|
||||||
|
|
||||||
|
|
||||||
benches/
|
benches/
|
||||||
--------
|
^^^^^^^^
|
||||||
|
|
||||||
This folder contains different benchmarks.
|
This folder contains different benchmarks.
|
||||||
Pretty straightforward.
|
Pretty straightforward.
|
||||||
|
|
||||||
|
|
||||||
stubs/
|
stubs/
|
||||||
------
|
^^^^^^
|
||||||
|
|
||||||
If a dependency doesn't support typing, files here must work around that.
|
If a dependency doesn't support typing, files here must work around that.
|
||||||
|
|
||||||
|
.. _tools:
|
||||||
|
|
||||||
tools/
|
tools/
|
||||||
------
|
^^^^^^
|
||||||
|
|
||||||
Various utility scripts.
|
Various utility scripts.
|
||||||
Each script should have a "comment" at the top explaining what they are for.
|
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 <https://www.sphinx-doc.org>`_ and `graphviz <https://www.graphviz.org>`_'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/
|
generator/
|
||||||
----------
|
^^^^^^^^^^
|
||||||
|
|
||||||
A package that should not be published and is only used when developing the library.
|
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.
|
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.
|
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.
|
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/
|
client/
|
||||||
-------
|
^^^^^^^
|
||||||
|
|
||||||
The Telethon client library and documentation lives here.
|
The Telethon client library and documentation lives here.
|
||||||
This is the package that gets published.
|
This is the package that gets published.
|
|
@ -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``.
|
``.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
|
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).
|
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
|
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 <https://pypi.org/project/jsonpickle/>`_.
|
If you need to serialize the session data to a string, you can use something like `jsonpickle <https://pypi.org/project/jsonpickle/>`_.
|
||||||
Or even the built-in :mod:`pickle` followed by :mod:`base64` or just :meth:`bytes.hex`.
|
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`.
|
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.
|
|
||||||
|
|
|
@ -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.
|
|
|
@ -123,6 +123,4 @@ Tips and tricks to develop both with the library and for the library.
|
||||||
developing/changelog
|
developing/changelog
|
||||||
developing/migration-guide
|
developing/migration-guide
|
||||||
developing/faq
|
developing/faq
|
||||||
developing/philosophy.rst
|
developing/contributing
|
||||||
developing/coding-style.rst
|
|
||||||
developing/project-structure.rst
|
|
||||||
|
|
|
@ -1,4 +1 @@
|
||||||
Client
|
|
||||||
======
|
|
||||||
|
|
||||||
.. autoclass:: telethon.Client
|
.. autoclass:: telethon.Client
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
"""
|
||||||
|
The main package for the Telethon library.
|
||||||
|
"""
|
||||||
from ._impl import tl as _tl
|
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.client.errors import errors
|
||||||
from ._impl.mtproto import RpcError
|
from ._impl.mtproto import RpcError
|
||||||
from ._impl.session import Session
|
|
||||||
from .version import __version__
|
from .version import __version__
|
||||||
|
|
||||||
__all__ = ["_tl", "Client", "Config", "errors", "RpcError", "Session"]
|
__all__ = ["_tl", "Client", "errors", "RpcError"]
|
||||||
|
|
|
@ -9,7 +9,7 @@ from ...mtproto import RpcError
|
||||||
from ...session import User as SessionUser
|
from ...session import User as SessionUser
|
||||||
from ...tl import abcs, functions, types
|
from ...tl import abcs, functions, types
|
||||||
from ..types import LoginToken, PasswordToken, User
|
from ..types import LoginToken, PasswordToken, User
|
||||||
from .net import connect_sender
|
from .net import connect_sender, datacenter_for_id
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .client import Client
|
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:
|
async def complete_login(client: Client, auth: abcs.auth.Authorization) -> User:
|
||||||
|
assert client._sender
|
||||||
assert isinstance(auth, types.auth.Authorization)
|
assert isinstance(auth, types.auth.Authorization)
|
||||||
assert isinstance(auth.user, types.User)
|
assert isinstance(auth.user, types.User)
|
||||||
user = User._from_raw(auth.user)
|
user = User._from_raw(auth.user)
|
||||||
client._config.session.user = SessionUser(
|
client._session.user = SessionUser(
|
||||||
id=user.id, dc=client._dc_id, bot=user.bot, username=user.username
|
id=user.id, dc=client._sender.dc_id, bot=user.bot, username=user.username
|
||||||
)
|
)
|
||||||
|
|
||||||
packed = user.pack()
|
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:
|
async def handle_migrate(client: Client, dc_id: Optional[int]) -> None:
|
||||||
assert dc_id is not 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:
|
async with client._sender_lock:
|
||||||
client._sender = sender
|
client._sender = sender
|
||||||
client._dc_id = dc_id
|
|
||||||
|
|
||||||
|
|
||||||
async def bot_sign_in(self: Client, token: str) -> User:
|
async def bot_sign_in(self: Client, token: str) -> User:
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from __future__ import annotations
|
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 ...tl import abcs, functions, types
|
||||||
from ..types import ChatLike, Message, NoPublicConstructor
|
from ..types import ChatLike, InlineResult, NoPublicConstructor
|
||||||
from ..utils import generate_random_id
|
from ..utils import generate_random_id
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -64,64 +64,6 @@ class InlineResults(metaclass=NoPublicConstructor):
|
||||||
return self._buffer.pop()
|
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(
|
async def inline_query(
|
||||||
self: Client, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None
|
self: Client, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None
|
||||||
) -> AsyncIterator[InlineResult]:
|
) -> AsyncIterator[InlineResult]:
|
||||||
|
|
|
@ -18,6 +18,9 @@ from typing import (
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from telethon._impl.session.session import DataCenter
|
||||||
|
|
||||||
|
from ....version import __version__ as default_version
|
||||||
from ...mtsender import Sender
|
from ...mtsender import Sender
|
||||||
from ...session import (
|
from ...session import (
|
||||||
ChatHashCache,
|
ChatHashCache,
|
||||||
|
@ -93,6 +96,8 @@ from .net import (
|
||||||
Config,
|
Config,
|
||||||
connect,
|
connect,
|
||||||
connected,
|
connected,
|
||||||
|
default_device_model,
|
||||||
|
default_system_version,
|
||||||
disconnect,
|
disconnect,
|
||||||
invoke_request,
|
invoke_request,
|
||||||
run_until_disconnected,
|
run_until_disconnected,
|
||||||
|
@ -137,9 +142,13 @@ class Client:
|
||||||
:param api_id:
|
:param api_id:
|
||||||
The API ID. See :doc:`/basic/signing-in` to learn how to obtain it.
|
The API ID. See :doc:`/basic/signing-in` to learn how to obtain it.
|
||||||
|
|
||||||
|
This is required to initialize the connection.
|
||||||
|
|
||||||
:param api_hash:
|
:param api_hash:
|
||||||
The API hash. See :doc:`/basic/signing-in` to learn how to obtain it.
|
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:
|
:param device_model:
|
||||||
Device model.
|
Device model.
|
||||||
|
|
||||||
|
@ -158,8 +167,8 @@ class Client:
|
||||||
:param catch_up:
|
:param catch_up:
|
||||||
Whether to "catch up" on updates that occured while the client was not connected.
|
Whether to "catch up" on updates that occured while the client was not connected.
|
||||||
|
|
||||||
:param server_addr:
|
:param datacenter:
|
||||||
Override the server address ``'ip:port'`` pair to connect to.
|
Override the datacenter to connect to.
|
||||||
Useful to connect to one of Telegram's test servers.
|
Useful to connect to one of Telegram's test servers.
|
||||||
|
|
||||||
:param flood_sleep_threshold:
|
:param flood_sleep_threshold:
|
||||||
|
@ -183,23 +192,45 @@ class Client:
|
||||||
session: Optional[Union[str, Path, Storage]],
|
session: Optional[Union[str, Path, Storage]],
|
||||||
api_id: int,
|
api_id: int,
|
||||||
api_hash: Optional[str] = None,
|
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,
|
check_all_handlers: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._sender: Optional[Sender] = None
|
self._sender: Optional[Sender] = None
|
||||||
self._sender_lock = asyncio.Lock()
|
self._sender_lock = asyncio.Lock()
|
||||||
self._dc_id = DEFAULT_DC
|
|
||||||
if isinstance(session, Storage):
|
if isinstance(session, Storage):
|
||||||
self._storage = session
|
self._storage = session
|
||||||
elif session is None:
|
elif session is None:
|
||||||
self._storage = MemorySession()
|
self._storage = MemorySession()
|
||||||
else:
|
else:
|
||||||
self._storage = SqliteSession(session)
|
self._storage = SqliteSession(session)
|
||||||
|
|
||||||
self._config = Config(
|
self._config = Config(
|
||||||
session=Session(),
|
|
||||||
api_id=api_id,
|
api_id=api_id,
|
||||||
api_hash=api_hash or "",
|
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._message_box = MessageBox()
|
||||||
self._chat_hashes = ChatHashCache(None)
|
self._chat_hashes = ChatHashCache(None)
|
||||||
self._last_update_limit_warn: Optional[float] = None
|
self._last_update_limit_warn: Optional[float] = None
|
||||||
|
@ -212,10 +243,8 @@ class Client:
|
||||||
] = {}
|
] = {}
|
||||||
self._shortcircuit_handlers = not check_all_handlers
|
self._shortcircuit_handlers = not check_all_handlers
|
||||||
|
|
||||||
if self_user := self._config.session.user:
|
if self._session.user and self._config.catch_up and self._session.state:
|
||||||
self._dc_id = self_user.dc
|
self._message_box.load(self._session.state)
|
||||||
if self._config.catch_up and self._config.session.state:
|
|
||||||
self._message_box.load(self._config.session.state)
|
|
||||||
|
|
||||||
# Begin partially @generated
|
# Begin partially @generated
|
||||||
|
|
||||||
|
@ -468,7 +497,10 @@ class Client:
|
||||||
:param message_id:
|
:param message_id:
|
||||||
The identifier of the message to edit.
|
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.
|
:return: The edited message.
|
||||||
|
|
||||||
|
@ -728,6 +760,27 @@ class Client:
|
||||||
def get_messages_with_ids(
|
def get_messages_with_ids(
|
||||||
self, chat: ChatLike, message_ids: List[int]
|
self, chat: ChatLike, message_ids: List[int]
|
||||||
) -> AsyncList[Message]:
|
) -> 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)
|
return get_messages_with_ids(self, chat, message_ids)
|
||||||
|
|
||||||
def get_participants(self, chat: ChatLike) -> AsyncList[Participant]:
|
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.
|
It is very likely that you will not be able to fetch all the members.
|
||||||
There is no way to bypass this.
|
There is no way to bypass this.
|
||||||
|
|
||||||
|
:param chat:
|
||||||
|
The :term:`chat` to fetch participants from.
|
||||||
|
|
||||||
:return: The participants.
|
:return: The participants.
|
||||||
|
|
||||||
.. rubric:: Example
|
.. rubric:: Example
|
||||||
|
@ -753,6 +809,9 @@ class Client:
|
||||||
"""
|
"""
|
||||||
Get the profile pictures set in a chat, or user avatars.
|
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.
|
:return: The photo files.
|
||||||
|
|
||||||
.. rubric:: Example
|
.. rubric:: Example
|
||||||
|
@ -990,9 +1049,32 @@ class Client:
|
||||||
return await resolve_to_packed(self, chat)
|
return await resolve_to_packed(self, chat)
|
||||||
|
|
||||||
async def resolve_username(self, username: str) -> 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)
|
return await resolve_username(self, username)
|
||||||
|
|
||||||
async def run_until_disconnected(self) -> None:
|
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)
|
await run_until_disconnected(self)
|
||||||
|
|
||||||
def search_all_messages(
|
def search_all_messages(
|
||||||
|
@ -1084,10 +1166,8 @@ class Client:
|
||||||
async def send_audio(
|
async def send_audio(
|
||||||
self,
|
self,
|
||||||
chat: ChatLike,
|
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,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
duration: Optional[float] = None,
|
duration: Optional[float] = None,
|
||||||
|
@ -1105,12 +1185,18 @@ class Client:
|
||||||
duration, title and performer if they are not provided.
|
duration, title and performer if they are not provided.
|
||||||
|
|
||||||
:param chat:
|
: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:
|
:param file: See :meth:`send_file`.
|
||||||
A local file path or :class:`~telethon.types.File` to send.
|
:param size: See :meth:`send_file`.
|
||||||
|
:param name: See :meth:`send_file`.
|
||||||
The rest of parameters behave the same as they do in :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
|
.. rubric:: Example
|
||||||
|
|
||||||
|
@ -1121,9 +1207,7 @@ class Client:
|
||||||
return await send_audio(
|
return await send_audio(
|
||||||
self,
|
self,
|
||||||
chat,
|
chat,
|
||||||
path,
|
file,
|
||||||
url=url,
|
|
||||||
file=file,
|
|
||||||
size=size,
|
size=size,
|
||||||
name=name,
|
name=name,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
|
@ -1138,10 +1222,8 @@ class Client:
|
||||||
async def send_file(
|
async def send_file(
|
||||||
self,
|
self,
|
||||||
chat: ChatLike,
|
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,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
mime_type: Optional[str] = None,
|
mime_type: Optional[str] = None,
|
||||||
|
@ -1152,7 +1234,6 @@ class Client:
|
||||||
title: Optional[str] = None,
|
title: Optional[str] = None,
|
||||||
performer: Optional[str] = None,
|
performer: Optional[str] = None,
|
||||||
emoji: Optional[str] = None,
|
emoji: Optional[str] = None,
|
||||||
emoji_sticker: Optional[str] = None,
|
|
||||||
width: Optional[int] = None,
|
width: Optional[int] = None,
|
||||||
height: Optional[int] = None,
|
height: Optional[int] = None,
|
||||||
round: bool = False,
|
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:
|
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`.
|
`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:
|
:param chat:
|
||||||
The :term:`chat` where the message will be sent to.
|
The :term:`chat` where the message will be sent to.
|
||||||
|
@ -1177,36 +1258,121 @@ class Client:
|
||||||
:param path:
|
:param path:
|
||||||
A local file path or :class:`~telethon.types.File` to send.
|
A local file path or :class:`~telethon.types.File` to send.
|
||||||
|
|
||||||
:param caption:
|
:param file:
|
||||||
Caption text to display under the media, with no formatting.
|
The file to send.
|
||||||
|
|
||||||
:param caption_markdown:
|
This can be a path, relative or absolute, to a local file, as either a :class:`str` or :class:`pathlib.Path`.
|
||||||
Caption text to display under the media, parsed as markdown.
|
|
||||||
|
|
||||||
:param caption_html:
|
It can also be a file opened for reading in binary mode, with its ``read`` method optionally being ``async``.
|
||||||
Caption text to display under the media, parsed as HTML.
|
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 you wrote to an in-memory file, you probably want to ``file.seek(0)`` first.
|
||||||
if *path* isn't a :class:`~telethon.types.File`.
|
If you want to send :class:`bytes`, wrap them in :class:`io.BytesIO` first.
|
||||||
See the documentation of :meth:`~telethon.types.File.new` to learn what they do.
|
|
||||||
|
|
||||||
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
|
.. rubric:: Example
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
login_token = await client.request_login_code('+1 23 456...')
|
await client.send_file(chat, 'picture.jpg')
|
||||||
print(login_token.timeout, 'seconds before code expires')
|
|
||||||
|
# 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(
|
return await send_file(
|
||||||
self,
|
self,
|
||||||
chat,
|
chat,
|
||||||
path,
|
file,
|
||||||
url=url,
|
|
||||||
file=file,
|
|
||||||
size=size,
|
size=size,
|
||||||
name=name,
|
name=name,
|
||||||
mime_type=mime_type,
|
mime_type=mime_type,
|
||||||
|
@ -1217,7 +1383,6 @@ class Client:
|
||||||
title=title,
|
title=title,
|
||||||
performer=performer,
|
performer=performer,
|
||||||
emoji=emoji,
|
emoji=emoji,
|
||||||
emoji_sticker=emoji_sticker,
|
|
||||||
width=width,
|
width=width,
|
||||||
height=height,
|
height=height,
|
||||||
round=round,
|
round=round,
|
||||||
|
@ -1244,16 +1409,9 @@ class Client:
|
||||||
:param chat:
|
:param chat:
|
||||||
The :term:`chat` where the message will be sent to.
|
The :term:`chat` where the message will be sent to.
|
||||||
|
|
||||||
:param text:
|
:param text: See :ref:`formatting`.
|
||||||
Message text, with no formatting.
|
:param markdown: See :ref:`formatting`.
|
||||||
|
:param html: See :ref:`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 link_preview:
|
:param link_preview:
|
||||||
Whether the link preview is allowed.
|
Whether the link preview is allowed.
|
||||||
|
@ -1266,10 +1424,6 @@ class Client:
|
||||||
:param reply_to:
|
:param reply_to:
|
||||||
The message identifier of the message to 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
|
.. rubric:: Example
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -1289,10 +1443,8 @@ class Client:
|
||||||
async def send_photo(
|
async def send_photo(
|
||||||
self,
|
self,
|
||||||
chat: ChatLike,
|
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,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
compress: bool = True,
|
compress: bool = True,
|
||||||
|
@ -1309,16 +1461,21 @@ class Client:
|
||||||
Only compressed images can be displayed as photos in applications.
|
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.
|
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.
|
width and height if they are not provided.
|
||||||
|
|
||||||
:param chat:
|
: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:
|
:param file: See :meth:`send_file`.
|
||||||
A local file path or :class:`~telethon.types.File` to send.
|
:param size: See :meth:`send_file`.
|
||||||
|
:param name: See :meth:`send_file`.
|
||||||
The rest of parameters behave the same as they do in :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
|
.. rubric:: Example
|
||||||
|
|
||||||
|
@ -1329,9 +1486,7 @@ class Client:
|
||||||
return await send_photo(
|
return await send_photo(
|
||||||
self,
|
self,
|
||||||
chat,
|
chat,
|
||||||
path,
|
file,
|
||||||
url=url,
|
|
||||||
file=file,
|
|
||||||
size=size,
|
size=size,
|
||||||
name=name,
|
name=name,
|
||||||
compress=compress,
|
compress=compress,
|
||||||
|
@ -1345,10 +1500,8 @@ class Client:
|
||||||
async def send_video(
|
async def send_video(
|
||||||
self,
|
self,
|
||||||
chat: ChatLike,
|
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,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
duration: Optional[float] = None,
|
duration: Optional[float] = None,
|
||||||
|
@ -1363,16 +1516,23 @@ class Client:
|
||||||
"""
|
"""
|
||||||
Send a video file.
|
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.
|
duration, width and height if they are not provided.
|
||||||
|
|
||||||
:param chat:
|
:param chat:
|
||||||
The :term:`chat` where the message will be sent to.
|
The :term:`chat` where the message will be sent to.
|
||||||
|
|
||||||
:param path:
|
:param file: See :meth:`send_file`.
|
||||||
A local file path or :class:`~telethon.types.File` to send.
|
:param size: See :meth:`send_file`.
|
||||||
|
:param name: See :meth:`send_file`.
|
||||||
The rest of parameters behave the same as they do in :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
|
.. rubric:: Example
|
||||||
|
|
||||||
|
@ -1383,9 +1543,7 @@ class Client:
|
||||||
return await send_video(
|
return await send_video(
|
||||||
self,
|
self,
|
||||||
chat,
|
chat,
|
||||||
path,
|
file,
|
||||||
url=url,
|
|
||||||
file=file,
|
|
||||||
size=size,
|
size=size,
|
||||||
name=name,
|
name=name,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
|
@ -1442,6 +1600,10 @@ class Client:
|
||||||
:param token:
|
:param token:
|
||||||
The login token returned from :meth:`request_login_code`.
|
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:
|
:return:
|
||||||
The user corresponding to :term:`yourself`, or a password token if the account has 2FA enabled.
|
The user corresponding to :term:`yourself`, or a password token if the account has 2FA enabled.
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import math
|
||||||
|
import mimetypes
|
||||||
|
import urllib.parse
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from pathlib import Path
|
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 ...tl import abcs, functions, types
|
||||||
from ..types import (
|
from ..types import (
|
||||||
|
@ -28,14 +31,15 @@ MAX_CHUNK_SIZE = 512 * 1024
|
||||||
FILE_MIGRATE_ERROR = 303
|
FILE_MIGRATE_ERROR = 303
|
||||||
BIG_FILE_SIZE = 10 * 1024 * 1024
|
BIG_FILE_SIZE = 10 * 1024 * 1024
|
||||||
|
|
||||||
|
# ``round`` parameter would make this more annoying to access otherwise.
|
||||||
|
math_round = round
|
||||||
|
|
||||||
|
|
||||||
async def send_photo(
|
async def send_photo(
|
||||||
self: Client,
|
self: Client,
|
||||||
chat: ChatLike,
|
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,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
compress: bool = True,
|
compress: bool = True,
|
||||||
|
@ -48,9 +52,7 @@ async def send_photo(
|
||||||
return await send_file(
|
return await send_file(
|
||||||
self,
|
self,
|
||||||
chat,
|
chat,
|
||||||
path,
|
file,
|
||||||
url=url,
|
|
||||||
file=file,
|
|
||||||
size=size,
|
size=size,
|
||||||
name=name,
|
name=name,
|
||||||
compress=compress,
|
compress=compress,
|
||||||
|
@ -65,10 +67,8 @@ async def send_photo(
|
||||||
async def send_audio(
|
async def send_audio(
|
||||||
self: Client,
|
self: Client,
|
||||||
chat: ChatLike,
|
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,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
duration: Optional[float] = None,
|
duration: Optional[float] = None,
|
||||||
|
@ -82,9 +82,7 @@ async def send_audio(
|
||||||
return await send_file(
|
return await send_file(
|
||||||
self,
|
self,
|
||||||
chat,
|
chat,
|
||||||
path,
|
file,
|
||||||
url=url,
|
|
||||||
file=file,
|
|
||||||
size=size,
|
size=size,
|
||||||
name=name,
|
name=name,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
|
@ -100,10 +98,8 @@ async def send_audio(
|
||||||
async def send_video(
|
async def send_video(
|
||||||
self: Client,
|
self: Client,
|
||||||
chat: ChatLike,
|
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,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
duration: Optional[float] = None,
|
duration: Optional[float] = None,
|
||||||
|
@ -118,9 +114,7 @@ async def send_video(
|
||||||
return await send_file(
|
return await send_file(
|
||||||
self,
|
self,
|
||||||
chat,
|
chat,
|
||||||
path,
|
file,
|
||||||
url=url,
|
|
||||||
file=file,
|
|
||||||
size=size,
|
size=size,
|
||||||
name=name,
|
name=name,
|
||||||
duration=duration,
|
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(
|
async def send_file(
|
||||||
self: Client,
|
self: Client,
|
||||||
chat: ChatLike,
|
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,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
mime_type: Optional[str] = None,
|
mime_type: Optional[str] = None,
|
||||||
|
@ -161,61 +162,127 @@ async def send_file(
|
||||||
caption_markdown: Optional[str] = None,
|
caption_markdown: Optional[str] = None,
|
||||||
caption_html: Optional[str] = None,
|
caption_html: Optional[str] = None,
|
||||||
) -> Message:
|
) -> 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(
|
message, entities = parse_message(
|
||||||
text=caption, markdown=caption_markdown, html=caption_html, allow_empty=True
|
text=caption, markdown=caption_markdown, html=caption_html, allow_empty=True
|
||||||
)
|
)
|
||||||
assert isinstance(message, str)
|
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:
|
# URLs are handled early as they can't use any other attributes either.
|
||||||
if file_info._input_file is None:
|
input_media: abcs.InputMedia
|
||||||
file_info._input_file = await upload(self, file_info)
|
if (url_path := try_get_url_path(file)) is not None:
|
||||||
file_info._input_media = (
|
assert isinstance(file, str)
|
||||||
types.InputMediaUploadedPhoto(
|
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
|
||||||
|
)
|
||||||
|
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,
|
spoiler=False,
|
||||||
file=file_info._input_file,
|
file=input_file,
|
||||||
stickers=None,
|
stickers=None,
|
||||||
ttl_seconds=None,
|
ttl_seconds=None,
|
||||||
)
|
)
|
||||||
if file_info._photo
|
|
||||||
else types.InputMediaUploadedDocument(
|
# Only bother to calculate attributes when sending documents.
|
||||||
nosound_video=file_info._muted,
|
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,
|
force_file=False,
|
||||||
spoiler=False,
|
spoiler=False,
|
||||||
file=file_info._input_file,
|
file=input_file,
|
||||||
thumb=None,
|
thumb=None,
|
||||||
mime_type=file_info._mime,
|
mime_type=mime_type,
|
||||||
attributes=file_info._attributes,
|
attributes=attributes,
|
||||||
stickers=None,
|
stickers=None,
|
||||||
ttl_seconds=None,
|
ttl_seconds=None,
|
||||||
)
|
)
|
||||||
)
|
|
||||||
random_id = generate_random_id()
|
|
||||||
|
|
||||||
return self._build_message_map(
|
return await do_send_file(self, chat, input_media, message, entities)
|
||||||
await self(
|
|
||||||
|
|
||||||
|
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(
|
functions.messages.send_media(
|
||||||
silent=False,
|
silent=False,
|
||||||
background=False,
|
background=False,
|
||||||
|
@ -224,7 +291,7 @@ async def send_file(
|
||||||
update_stickersets_order=False,
|
update_stickersets_order=False,
|
||||||
peer=peer,
|
peer=peer,
|
||||||
reply_to=None,
|
reply_to=None,
|
||||||
media=file_info._input_media,
|
media=input_media,
|
||||||
message=message,
|
message=message,
|
||||||
random_id=random_id,
|
random_id=random_id,
|
||||||
reply_markup=None,
|
reply_markup=None,
|
||||||
|
@ -239,26 +306,28 @@ async def send_file(
|
||||||
|
|
||||||
async def upload(
|
async def upload(
|
||||||
client: Client,
|
client: Client,
|
||||||
file: File,
|
fd: InFileLike,
|
||||||
|
size: int,
|
||||||
|
name: str,
|
||||||
) -> abcs.InputFile:
|
) -> abcs.InputFile:
|
||||||
file_id = generate_random_id()
|
file_id = generate_random_id()
|
||||||
|
|
||||||
uploaded = 0
|
uploaded = 0
|
||||||
part = 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()
|
buffer = bytearray()
|
||||||
to_store: Union[bytearray, bytes] = b""
|
to_store: Union[bytearray, bytes] = b""
|
||||||
hash_md5 = hashlib.md5()
|
hash_md5 = hashlib.md5()
|
||||||
is_big = file._size > BIG_FILE_SIZE
|
is_big = size > BIG_FILE_SIZE
|
||||||
|
|
||||||
fd = file._open()
|
while uploaded != size:
|
||||||
try:
|
ret = fd.read(MAX_CHUNK_SIZE - len(buffer))
|
||||||
while uploaded != file._size:
|
chunk = await ret if isawaitable(ret) else ret
|
||||||
chunk = await fd.read(MAX_CHUNK_SIZE - len(buffer))
|
assert isinstance(chunk, bytes)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise ValueError("unexpected end-of-file")
|
raise ValueError("unexpected end-of-file")
|
||||||
|
|
||||||
if len(chunk) == MAX_CHUNK_SIZE or uploaded + len(chunk) == file._size:
|
if len(chunk) == MAX_CHUNK_SIZE or uploaded + len(chunk) == size:
|
||||||
to_store = chunk
|
to_store = chunk
|
||||||
else:
|
else:
|
||||||
buffer += chunk
|
buffer += chunk
|
||||||
|
@ -286,20 +355,18 @@ async def upload(
|
||||||
|
|
||||||
buffer.clear()
|
buffer.clear()
|
||||||
part += 1
|
part += 1
|
||||||
finally:
|
|
||||||
fd.close()
|
|
||||||
|
|
||||||
if file._size > BIG_FILE_SIZE:
|
if is_big:
|
||||||
return types.InputFileBig(
|
return types.InputFileBig(
|
||||||
id=file_id,
|
id=file_id,
|
||||||
parts=total_parts,
|
parts=total_parts,
|
||||||
name=file._name,
|
name=name,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return types.InputFile(
|
return types.InputFile(
|
||||||
id=file_id,
|
id=file_id,
|
||||||
parts=total_parts,
|
parts=total_parts,
|
||||||
name=file._name,
|
name=name,
|
||||||
md5_checksum=hash_md5.hexdigest(),
|
md5_checksum=hash_md5.hexdigest(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import itertools
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
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 ....version import __version__
|
||||||
from ...mtproto import Full, RpcError
|
from ...mtproto import Full, RpcError
|
||||||
|
@ -13,7 +14,7 @@ from ...mtsender import connect as connect_without_auth
|
||||||
from ...mtsender import connect_with_auth
|
from ...mtsender import connect_with_auth
|
||||||
from ...session import DataCenter, Session
|
from ...session import DataCenter, Session
|
||||||
from ...session import User as SessionUser
|
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 ..errors import adapt_rpc
|
||||||
from .updates import dispatcher, process_socket_updates
|
from .updates import dispatcher, process_socket_updates
|
||||||
|
|
||||||
|
@ -41,13 +42,6 @@ def default_system_version() -> str:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Config:
|
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_id: int
|
||||||
api_hash: str
|
api_hash: str
|
||||||
device_model: str = field(default_factory=default_device_model)
|
device_model: str = field(default_factory=default_device_model)
|
||||||
|
@ -56,54 +50,33 @@ class Config:
|
||||||
system_lang_code: str = "en"
|
system_lang_code: str = "en"
|
||||||
lang_code: str = "en"
|
lang_code: str = "en"
|
||||||
catch_up: bool = False
|
catch_up: bool = False
|
||||||
server_addr: Optional[str] = None
|
datacenter: Optional[DataCenter] = None
|
||||||
flood_sleep_threshold: Optional[int] = 60
|
flood_sleep_threshold: Optional[int] = 60
|
||||||
update_queue_limit: Optional[int] = None
|
update_queue_limit: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
# dc_id to IPv4 and port pair
|
KNOWN_DC = [
|
||||||
DC_ADDRESSES = [
|
DataCenter(id=1, addr="149.154.175.53:443", auth=None),
|
||||||
"0.0.0.0:0",
|
DataCenter(id=2, addr="149.154.167.51:443", auth=None),
|
||||||
"149.154.175.53:443",
|
DataCenter(id=3, addr="149.154.175.100:443", auth=None),
|
||||||
"149.154.167.51:443",
|
DataCenter(id=4, addr="149.154.167.92:443", auth=None),
|
||||||
"149.154.175.100:443",
|
DataCenter(id=5, addr="91.108.56.190:443", auth=None),
|
||||||
"149.154.167.92:443",
|
|
||||||
"91.108.56.190:443",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_DC = 2
|
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()
|
transport = Full()
|
||||||
|
|
||||||
if config.server_addr:
|
|
||||||
addr = config.server_addr
|
|
||||||
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:
|
if dc.auth:
|
||||||
auth_key = dc.auth
|
sender = await connect_with_auth(transport, dc.id, dc.addr, dc.auth)
|
||||||
break
|
|
||||||
|
|
||||||
if auth_key:
|
|
||||||
sender = await connect_with_auth(transport, addr, auth_key)
|
|
||||||
else:
|
else:
|
||||||
sender = await connect_without_auth(transport, addr)
|
sender = await connect_without_auth(transport, dc.id, dc.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)
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO handle -404 (we had a previously-valid authkey, but server no longer knows about it)
|
# 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(
|
remote_config = await sender.invoke(
|
||||||
functions.invoke_with_layer(
|
functions.invoke_with_layer(
|
||||||
layer=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:
|
async def connect(self: Client) -> None:
|
||||||
|
@ -131,32 +124,44 @@ async def connect(self: Client) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if session := await self._storage.load():
|
if session := await self._storage.load():
|
||||||
self._config.session = session
|
self._session = session
|
||||||
|
|
||||||
if user := self._config.session.user:
|
if dc := self._config.datacenter:
|
||||||
self._dc_id = user.dc
|
# 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:
|
else:
|
||||||
for dc in self._config.session.dcs:
|
# Reuse the session's datacenter, falling back to defaults if not found.
|
||||||
if dc.auth:
|
datacenter = datacenter_for_id(
|
||||||
self._dc_id = dc.id
|
self, self._session.user.dc if self._session.user else DEFAULT_DC
|
||||||
break
|
)
|
||||||
|
|
||||||
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:
|
try:
|
||||||
await self(functions.updates.get_state())
|
await self(functions.updates.get_state())
|
||||||
except RpcError as e:
|
except RpcError as e:
|
||||||
if e.code == 401:
|
if e.code == 401:
|
||||||
self._config.session.user = None
|
self._session.user = None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if not self._config.session.user:
|
if not self._session.user:
|
||||||
me = await self.get_me()
|
me = await self.get_me()
|
||||||
assert me is not None
|
assert me is not None
|
||||||
self._config.session.user = SessionUser(
|
self._session.user = SessionUser(
|
||||||
id=me.id, dc=self._dc_id, bot=me.bot, username=me.username
|
id=me.id, dc=self._sender.dc_id, bot=me.bot, username=me.username
|
||||||
)
|
)
|
||||||
packed = me.pack()
|
packed = me.pack()
|
||||||
assert packed is not None
|
assert packed is not None
|
||||||
|
@ -165,6 +170,17 @@ async def connect(self: Client) -> None:
|
||||||
self._dispatcher = asyncio.create_task(dispatcher(self))
|
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:
|
async def disconnect(self: Client) -> None:
|
||||||
if not self._sender:
|
if not self._sender:
|
||||||
return
|
return
|
||||||
|
@ -173,8 +189,6 @@ async def disconnect(self: Client) -> None:
|
||||||
self._dispatcher.cancel()
|
self._dispatcher.cancel()
|
||||||
try:
|
try:
|
||||||
await self._dispatcher
|
await self._dispatcher
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # TODO log
|
pass # TODO log
|
||||||
finally:
|
finally:
|
||||||
|
@ -187,8 +201,8 @@ async def disconnect(self: Client) -> None:
|
||||||
finally:
|
finally:
|
||||||
self._sender = None
|
self._sender = None
|
||||||
|
|
||||||
self._config.session.state = self._message_box.session_state()
|
self._session.state = self._message_box.session_state()
|
||||||
await self._storage.save(self._config.session)
|
await self._storage.save(self._session)
|
||||||
|
|
||||||
|
|
||||||
async def invoke_request(
|
async def invoke_request(
|
||||||
|
|
|
@ -123,14 +123,24 @@ def extend_update_queue(
|
||||||
|
|
||||||
|
|
||||||
async def dispatcher(client: Client) -> None:
|
async def dispatcher(client: Client) -> None:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
while client.connected:
|
while client.connected:
|
||||||
try:
|
try:
|
||||||
await dispatch_next(client)
|
await dispatch_next(client)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
raise
|
return
|
||||||
except Exception:
|
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
|
# TODO proper logger
|
||||||
logging.exception("Unhandled exception in event handler")
|
logging.exception("Unhandled exception in event handler")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_next(client: Client) -> None:
|
async def dispatch_next(client: Client) -> None:
|
||||||
|
|
|
@ -76,10 +76,10 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
|
||||||
if isinstance(chat, types.InputPeerEmpty):
|
if isinstance(chat, types.InputPeerEmpty):
|
||||||
raise ValueError("Cannot resolve chat")
|
raise ValueError("Cannot resolve chat")
|
||||||
elif isinstance(chat, types.InputPeerSelf):
|
elif isinstance(chat, types.InputPeerSelf):
|
||||||
if not self._config.session.user:
|
if not self._session.user:
|
||||||
raise ValueError("Cannot resolve chat")
|
raise ValueError("Cannot resolve chat")
|
||||||
return PackedChat(
|
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,
|
id=self._chat_hashes.self_id,
|
||||||
access_hash=0, # TODO get hash
|
access_hash=0, # TODO get hash
|
||||||
)
|
)
|
||||||
|
@ -112,7 +112,7 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
|
||||||
if chat.startswith("+"):
|
if chat.startswith("+"):
|
||||||
resolved = await resolve_phone(self, chat)
|
resolved = await resolve_phone(self, chat)
|
||||||
elif chat == "me":
|
elif chat == "me":
|
||||||
if me := self._config.session.user:
|
if me := self._session.user:
|
||||||
return PackedChat(
|
return PackedChat(
|
||||||
ty=PackedType.BOT if me.bot else PackedType.USER,
|
ty=PackedType.BOT if me.bot else PackedType.USER,
|
||||||
id=me.id,
|
id=me.id,
|
||||||
|
|
|
@ -72,7 +72,7 @@ class Command:
|
||||||
self._username = ""
|
self._username = ""
|
||||||
client: Optional[Client]
|
client: Optional[Client]
|
||||||
if (client := getattr(event, "_client", None)) is not None:
|
if (client := getattr(event, "_client", None)) is not None:
|
||||||
user = client._config.session.user
|
user = client._session.user
|
||||||
if user and user.username:
|
if user and user.username:
|
||||||
self._username = user.username
|
self._username = user.username
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,13 @@ from .async_list import AsyncList
|
||||||
from .chat import Channel, Chat, ChatLike, Group, RestrictionReason, User
|
from .chat import Channel, Chat, ChatLike, Group, RestrictionReason, User
|
||||||
from .dialog import Dialog
|
from .dialog import Dialog
|
||||||
from .draft import Draft
|
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 .login_token import LoginToken
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .meta import NoPublicConstructor
|
from .meta import NoPublicConstructor
|
||||||
from .participant import Participant
|
from .participant import Participant
|
||||||
from .password_token import PasswordToken
|
from .password_token import PasswordToken
|
||||||
|
from .inline_result import InlineResult
|
||||||
from .recent_action import RecentAction
|
from .recent_action import RecentAction
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -22,9 +23,9 @@ __all__ = [
|
||||||
"Draft",
|
"Draft",
|
||||||
"File",
|
"File",
|
||||||
"InFileLike",
|
"InFileLike",
|
||||||
"InWrapper",
|
|
||||||
"OutFileLike",
|
"OutFileLike",
|
||||||
"OutWrapper",
|
"OutWrapper",
|
||||||
|
"InlineResult",
|
||||||
"LoginToken",
|
"LoginToken",
|
||||||
"Message",
|
"Message",
|
||||||
"NoPublicConstructor",
|
"NoPublicConstructor",
|
||||||
|
|
|
@ -44,7 +44,7 @@ stripped_size_header = bytes.fromhex(
|
||||||
stripped_size_footer = bytes.fromhex("FFD9")
|
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 = bytearray(stripped_size_header)
|
||||||
header[164] = data[1]
|
header[164] = data[1]
|
||||||
header[166] = data[2]
|
header[166] = data[2]
|
||||||
|
@ -77,7 +77,12 @@ class InFileLike(Protocol):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def read(self, n: int) -> Union[bytes, Coroutine[Any, Any, bytes]]:
|
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):
|
class OutFileLike(Protocol):
|
||||||
|
@ -87,33 +92,12 @@ class OutFileLike(Protocol):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def write(self, data: bytes) -> Union[Any, Coroutine[Any, Any, Any]]:
|
def write(self, data: bytes) -> Union[Any, Coroutine[Any, Any, Any]]:
|
||||||
pass
|
"""
|
||||||
|
Write all the data into the file or buffer.
|
||||||
|
|
||||||
|
:param data:
|
||||||
class InWrapper:
|
Data that must be written to the buffer entirely.
|
||||||
__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()
|
|
||||||
|
|
||||||
|
|
||||||
class OutWrapper:
|
class OutWrapper:
|
||||||
|
@ -143,30 +127,24 @@ class OutWrapper:
|
||||||
|
|
||||||
class File(metaclass=NoPublicConstructor):
|
class File(metaclass=NoPublicConstructor):
|
||||||
"""
|
"""
|
||||||
File information of uploaded media.
|
File information of media sent to Telegram that can be downloaded.
|
||||||
|
|
||||||
It is used both when sending files or accessing media in a `Message`.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
path: Optional[Path],
|
|
||||||
file: Optional[InFileLike],
|
|
||||||
attributes: List[abcs.DocumentAttribute],
|
attributes: List[abcs.DocumentAttribute],
|
||||||
size: int,
|
size: int,
|
||||||
name: str,
|
name: str,
|
||||||
mime: str,
|
mime: str,
|
||||||
photo: bool,
|
photo: bool,
|
||||||
muted: bool,
|
muted: bool,
|
||||||
input_media: Optional[abcs.InputMedia],
|
input_media: abcs.InputMedia,
|
||||||
thumb: Optional[abcs.PhotoSize],
|
thumb: Optional[abcs.PhotoSize],
|
||||||
thumbs: Optional[List[abcs.PhotoSize]],
|
thumbs: Optional[List[abcs.PhotoSize]],
|
||||||
raw: Optional[Union[abcs.MessageMedia, abcs.Photo, abcs.Document]],
|
raw: Optional[Union[abcs.MessageMedia, abcs.Photo, abcs.Document]],
|
||||||
client: Optional[Client],
|
client: Optional[Client],
|
||||||
):
|
):
|
||||||
self._path = path
|
|
||||||
self._file = file
|
|
||||||
self._attributes = attributes
|
self._attributes = attributes
|
||||||
self._size = size
|
self._size = size
|
||||||
self._name = name
|
self._name = name
|
||||||
|
@ -226,8 +204,6 @@ class File(metaclass=NoPublicConstructor):
|
||||||
) -> Optional[Self]:
|
) -> Optional[Self]:
|
||||||
if isinstance(raw, types.Document):
|
if isinstance(raw, types.Document):
|
||||||
return cls._create(
|
return cls._create(
|
||||||
path=None,
|
|
||||||
file=None,
|
|
||||||
attributes=raw.attributes,
|
attributes=raw.attributes,
|
||||||
size=raw.size,
|
size=raw.size,
|
||||||
name=next(
|
name=next(
|
||||||
|
@ -279,8 +255,6 @@ class File(metaclass=NoPublicConstructor):
|
||||||
if isinstance(raw, types.Photo):
|
if isinstance(raw, types.Photo):
|
||||||
largest_thumb = max(raw.sizes, key=photo_size_byte_count)
|
largest_thumb = max(raw.sizes, key=photo_size_byte_count)
|
||||||
return cls._create(
|
return cls._create(
|
||||||
path=None,
|
|
||||||
file=None,
|
|
||||||
attributes=[],
|
attributes=[],
|
||||||
size=photo_size_byte_count(largest_thumb),
|
size=photo_size_byte_count(largest_thumb),
|
||||||
name="",
|
name="",
|
||||||
|
@ -304,165 +278,29 @@ class File(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@property
|
||||||
def new(
|
def name(self) -> Optional[str]:
|
||||||
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:
|
|
||||||
"""
|
"""
|
||||||
Create file information that can later be sent as media.
|
The file name, if known.
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
if isinstance(path, cls):
|
for attr in self._attributes:
|
||||||
return path
|
if isinstance(attr, types.DocumentAttributeFilename):
|
||||||
assert not isinstance(path, File)
|
return attr.file_name
|
||||||
|
|
||||||
attributes: List[abcs.DocumentAttribute] = []
|
return None
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ext(self) -> str:
|
def ext(self) -> str:
|
||||||
"""
|
"""
|
||||||
The file extension, including the leading dot ``.``.
|
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.
|
This makes it safe to always append this property to a file name.
|
||||||
"""
|
"""
|
||||||
if self._path:
|
if name := self._name:
|
||||||
return self._path.suffix
|
return Path(name).suffix
|
||||||
else:
|
else:
|
||||||
return mimetypes.guess_extension(self._mime) or ""
|
return mimetypes.guess_extension(self._mime) or ""
|
||||||
|
|
||||||
|
@ -477,8 +315,6 @@ class File(metaclass=NoPublicConstructor):
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
File._create(
|
File._create(
|
||||||
path=None,
|
|
||||||
file=None,
|
|
||||||
attributes=[],
|
attributes=[],
|
||||||
size=photo_size_byte_count(t),
|
size=photo_size_byte_count(t),
|
||||||
name="",
|
name="",
|
||||||
|
@ -530,22 +366,13 @@ class File(metaclass=NoPublicConstructor):
|
||||||
"""
|
"""
|
||||||
Alias for :meth:`telethon.Client.download`.
|
Alias for :meth:`telethon.Client.download`.
|
||||||
|
|
||||||
The file must have been obtained from Telegram to be downloadable.
|
:param file: See :meth:`~telethon.Client.download`.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
if not self._client:
|
if not self._client:
|
||||||
raise ValueError("only files from Telegram can be downloaded")
|
raise ValueError("only files from Telegram can be downloaded")
|
||||||
|
|
||||||
await self._client.download(self, file)
|
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:
|
def _input_location(self) -> abcs.InputFileLocation:
|
||||||
thumb_types = (
|
thumb_types = (
|
||||||
types.PhotoSizeEmpty,
|
types.PhotoSizeEmpty,
|
||||||
|
|
80
client/src/telethon/_impl/client/types/inline_result.py
Normal file
80
client/src/telethon/_impl/client/types/inline_result.py
Normal file
|
@ -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)
|
|
@ -186,7 +186,10 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
"""
|
"""
|
||||||
Alias for :meth:`telethon.Client.send_message`.
|
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(
|
return await self._client.send_message(
|
||||||
self.chat, text, markdown=markdown, html=html, link_preview=link_preview
|
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.
|
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(
|
return await self._client.send_message(
|
||||||
self.chat,
|
self.chat,
|
||||||
|
@ -218,7 +224,7 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
"""
|
"""
|
||||||
Alias for :meth:`telethon.Client.delete_messages`.
|
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)
|
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`.
|
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(
|
return await self._client.edit_message(
|
||||||
self.chat,
|
self.chat,
|
||||||
|
@ -247,17 +256,23 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
"""
|
"""
|
||||||
Alias for :meth:`telethon.Client.forward_messages`.
|
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]
|
return (await self._client.forward_messages(target, [self.id], self.chat))[0]
|
||||||
|
|
||||||
async def mark_read(self) -> None:
|
async def mark_read(self) -> None:
|
||||||
pass
|
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
|
pass
|
||||||
|
|
||||||
async def unpin(self) -> None:
|
async def unpin(self) -> None:
|
||||||
|
"""
|
||||||
|
Alias for :meth:`telethon.Client.unpin_message`.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# ---
|
# ---
|
||||||
|
|
|
@ -76,6 +76,8 @@ class Request(Generic[Return]):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Sender:
|
class Sender:
|
||||||
|
dc_id: int
|
||||||
|
addr: str
|
||||||
_reader: StreamReader
|
_reader: StreamReader
|
||||||
_writer: StreamWriter
|
_writer: StreamWriter
|
||||||
_transport: Transport
|
_transport: Transport
|
||||||
|
@ -88,10 +90,14 @@ class Sender:
|
||||||
_write_drain_pending: bool
|
_write_drain_pending: bool
|
||||||
|
|
||||||
@classmethod
|
@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(":"))
|
reader, writer = await asyncio.open_connection(*addr.split(":"))
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
|
dc_id=dc_id,
|
||||||
|
addr=addr,
|
||||||
_reader=reader,
|
_reader=reader,
|
||||||
_writer=writer,
|
_writer=writer,
|
||||||
_transport=transport,
|
_transport=transport,
|
||||||
|
@ -271,8 +277,8 @@ class Sender:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def connect(transport: Transport, addr: str) -> Sender:
|
async def connect(transport: Transport, dc_id: int, addr: str) -> Sender:
|
||||||
sender = await Sender.connect(transport, Plain(), addr)
|
sender = await Sender.connect(transport, Plain(), dc_id, addr)
|
||||||
return await generate_auth_key(sender)
|
return await generate_auth_key(sender)
|
||||||
|
|
||||||
|
|
||||||
|
@ -289,6 +295,8 @@ async def generate_auth_key(sender: Sender) -> Sender:
|
||||||
first_salt = finished.first_salt
|
first_salt = finished.first_salt
|
||||||
|
|
||||||
return Sender(
|
return Sender(
|
||||||
|
dc_id=sender.dc_id,
|
||||||
|
addr=sender.addr,
|
||||||
_reader=sender._reader,
|
_reader=sender._reader,
|
||||||
_writer=sender._writer,
|
_writer=sender._writer,
|
||||||
_transport=sender._transport,
|
_transport=sender._transport,
|
||||||
|
@ -304,9 +312,10 @@ async def generate_auth_key(sender: Sender) -> Sender:
|
||||||
|
|
||||||
async def connect_with_auth(
|
async def connect_with_auth(
|
||||||
transport: Transport,
|
transport: Transport,
|
||||||
|
dc_id: int,
|
||||||
addr: str,
|
addr: str,
|
||||||
auth_key: bytes,
|
auth_key: bytes,
|
||||||
) -> Sender:
|
) -> Sender:
|
||||||
return await Sender.connect(
|
return await Sender.connect(
|
||||||
transport, Encrypted(AuthKey.from_bytes(auth_key)), addr
|
transport, Encrypted(AuthKey.from_bytes(auth_key)), dc_id, addr
|
||||||
)
|
)
|
||||||
|
|
|
@ -67,6 +67,9 @@ class PackedChat:
|
||||||
"""
|
"""
|
||||||
Convenience method to convert hexadecimal numbers into bytes then passed to :meth:`from_bytes`:
|
Convenience method to convert hexadecimal numbers into bytes then passed to :meth:`from_bytes`:
|
||||||
|
|
||||||
|
:param hex:
|
||||||
|
Hexadecimal numbers to convert from.
|
||||||
|
|
||||||
.. code-block::
|
.. code-block::
|
||||||
|
|
||||||
assert PackedChat.from_hex(packed.hex) == packed
|
assert PackedChat.from_hex(packed.hex) == packed
|
||||||
|
|
|
@ -6,62 +6,71 @@ class DataCenter:
|
||||||
"""
|
"""
|
||||||
Data-center information.
|
Data-center information.
|
||||||
|
|
||||||
:var id: The DC identifier.
|
:param id: See below.
|
||||||
:var addr: The server address of the DC, in ``'ip:port'`` format.
|
:param addr: See below.
|
||||||
:var auth: Authentication key to encrypt communication with.
|
:param auth: See below.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("id", "addr", "auth")
|
__slots__ = ("id", "addr", "auth")
|
||||||
|
|
||||||
def __init__(self, *, id: int, addr: str, auth: Optional[bytes]) -> None:
|
def __init__(self, *, id: int, addr: str, auth: Optional[bytes]) -> None:
|
||||||
self.id = id
|
self.id = id
|
||||||
|
"The DC identifier."
|
||||||
self.addr = addr
|
self.addr = addr
|
||||||
|
"The server address of the DC, in ``'ip:port'`` format."
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
|
"Authentication key to encrypt communication with."
|
||||||
|
|
||||||
|
|
||||||
class User:
|
class User:
|
||||||
"""
|
"""
|
||||||
Information about the logged-in user.
|
Information about the logged-in user.
|
||||||
|
|
||||||
:var id: User identifier.
|
:param id: See below.
|
||||||
:var dc: Data-center identifier of the user's "home" DC.
|
:param dc: See below.
|
||||||
:var bot: :data:`True` if the user is from a bot account.
|
:param bot: See below.
|
||||||
:var username: User's primary username.
|
:param username: See below.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("id", "dc", "bot", "username")
|
__slots__ = ("id", "dc", "bot", "username")
|
||||||
|
|
||||||
def __init__(self, *, id: int, dc: int, bot: bool, username: Optional[str]) -> None:
|
def __init__(self, *, id: int, dc: int, bot: bool, username: Optional[str]) -> None:
|
||||||
self.id = id
|
self.id = id
|
||||||
|
"User identifier."
|
||||||
self.dc = dc
|
self.dc = dc
|
||||||
|
'Data-center identifier of the user\'s "home" DC.'
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
":data:`True` if the user is from a bot account."
|
||||||
self.username = username
|
self.username = username
|
||||||
|
"User's primary username."
|
||||||
|
|
||||||
|
|
||||||
class ChannelState:
|
class ChannelState:
|
||||||
"""
|
"""
|
||||||
Update state for a channel.
|
Update state for a channel.
|
||||||
|
|
||||||
:var id: The channel identifier.
|
:param id: See below.
|
||||||
:var pts: The channel's partial sequence number.
|
:param pts: See below.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("id", "pts")
|
__slots__ = ("id", "pts")
|
||||||
|
|
||||||
def __init__(self, *, id: int, pts: int) -> None:
|
def __init__(self, *, id: int, pts: int) -> None:
|
||||||
self.id = id
|
self.id = id
|
||||||
|
"The channel identifier."
|
||||||
self.pts = pts
|
self.pts = pts
|
||||||
|
"The channel's partial sequence number."
|
||||||
|
|
||||||
|
|
||||||
class UpdateState:
|
class UpdateState:
|
||||||
"""
|
"""
|
||||||
Update state for an account.
|
Update state for an account.
|
||||||
|
|
||||||
:var pts: The primary partial sequence number.
|
:param pts: See below.
|
||||||
:var qts: The secondary partial sequence number.
|
:param qts: See below.
|
||||||
:var date: Date of the latest update sequence.
|
:param date: See below.
|
||||||
:var seq: The sequence number.
|
:param seq: See below.
|
||||||
:var channels: Update state for channels.
|
:param channels: See below.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
|
@ -82,10 +91,15 @@ class UpdateState:
|
||||||
channels: List[ChannelState],
|
channels: List[ChannelState],
|
||||||
) -> None:
|
) -> None:
|
||||||
self.pts = pts
|
self.pts = pts
|
||||||
|
"The primary partial sequence number."
|
||||||
self.qts = qts
|
self.qts = qts
|
||||||
|
"The secondary partial sequence number."
|
||||||
self.date = date
|
self.date = date
|
||||||
|
"Date of the latest update sequence."
|
||||||
self.seq = seq
|
self.seq = seq
|
||||||
|
"The sequence number."
|
||||||
self.channels = channels
|
self.channels = channels
|
||||||
|
"Update state for channels."
|
||||||
|
|
||||||
|
|
||||||
class Session:
|
class Session:
|
||||||
|
@ -102,9 +116,18 @@ class Session:
|
||||||
|
|
||||||
If you think the session has been compromised, immediately terminate all
|
If you think the session has been compromised, immediately terminate all
|
||||||
sessions through an official Telegram client to revoke the authorization.
|
sessions through an official Telegram client to revoke the authorization.
|
||||||
|
|
||||||
|
:param dcs: See below.
|
||||||
|
:param user: See below.
|
||||||
|
:param state: See below.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
"""
|
||||||
|
Current version.
|
||||||
|
|
||||||
|
Will be incremented if new fields are added.
|
||||||
|
"""
|
||||||
|
|
||||||
__slots__ = ("dcs", "user", "state")
|
__slots__ = ("dcs", "user", "state")
|
||||||
|
|
||||||
|
@ -116,81 +139,8 @@ class Session:
|
||||||
state: Optional[UpdateState] = None,
|
state: Optional[UpdateState] = None,
|
||||||
):
|
):
|
||||||
self.dcs = dcs or []
|
self.dcs = dcs or []
|
||||||
|
"List of known data-centers."
|
||||||
self.user = user
|
self.user = user
|
||||||
|
"Information about the logged-in user."
|
||||||
self.state = state
|
self.state = state
|
||||||
|
"Update 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,
|
|
||||||
)
|
|
||||||
|
|
|
@ -12,17 +12,22 @@ class Storage(abc.ABC):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def load(self) -> Optional[Session]:
|
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`.
|
This method is called by the library prior to `connect`.
|
||||||
|
|
||||||
|
:return: The previously-saved session.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def save(self, session: Session) -> None:
|
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`.
|
This method is called by the library post `disconnect`.
|
||||||
|
|
||||||
|
:param session:
|
||||||
|
The session information that should be persisted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
|
|
@ -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 (
|
from .._impl.client.events import (
|
||||||
CallbackQuery,
|
CallbackQuery,
|
||||||
Event,
|
Event,
|
||||||
|
|
|
@ -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 (
|
from ._impl.session import (
|
||||||
ChannelState,
|
ChannelState,
|
||||||
DataCenter,
|
DataCenter,
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
"""
|
||||||
|
Classes for the various objects the library returns.
|
||||||
|
"""
|
||||||
from ._impl.client.client import Config, InlineResult
|
from ._impl.client.client import Config, InlineResult
|
||||||
from ._impl.client.types import (
|
from ._impl.client.types import (
|
||||||
AsyncList,
|
AsyncList,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import os
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from pytest import mark
|
from pytest import mark
|
||||||
from telethon import Client, Config, Session
|
from telethon import Client
|
||||||
from telethon import _tl as tl
|
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))
|
pong = await client(tl.mtproto.functions.ping(ping_id=ping_id))
|
||||||
assert isinstance(pong, tl.mtproto.types.Pong)
|
assert isinstance(pong, tl.mtproto.types.Pong)
|
||||||
assert pong.ping_id == ping_id
|
assert pong.ping_id == ping_id
|
||||||
|
|
||||||
|
await client.disconnect()
|
||||||
|
|
|
@ -4,11 +4,10 @@ import logging
|
||||||
from pytest import LogCaptureFixture, mark
|
from pytest import LogCaptureFixture, mark
|
||||||
from telethon._impl.mtproto import Full
|
from telethon._impl.mtproto import Full
|
||||||
from telethon._impl.mtsender import connect
|
from telethon._impl.mtsender import connect
|
||||||
|
from telethon._impl.session import DataCenter
|
||||||
from telethon._impl.tl import LAYER, abcs, functions, types
|
from telethon._impl.tl import LAYER, abcs, functions, types
|
||||||
|
|
||||||
TELEGRAM_TEST_DC_2 = "149.154.167.40:443"
|
TELEGRAM_TEST_DC = 2, "149.154.167.40:443"
|
||||||
|
|
||||||
TELEGRAM_DEFAULT_TEST_DC = TELEGRAM_TEST_DC_2
|
|
||||||
|
|
||||||
TEST_TIMEOUT = 10000
|
TEST_TIMEOUT = 10000
|
||||||
|
|
||||||
|
@ -22,9 +21,7 @@ async def test_invoke_encrypted_method(caplog: LogCaptureFixture) -> None:
|
||||||
def timeout() -> float:
|
def timeout() -> float:
|
||||||
return deadline - asyncio.get_running_loop().time()
|
return deadline - asyncio.get_running_loop().time()
|
||||||
|
|
||||||
sender = await asyncio.wait_for(
|
sender = await asyncio.wait_for(connect(Full(), *TELEGRAM_TEST_DC), timeout())
|
||||||
connect(Full(), TELEGRAM_DEFAULT_TEST_DC), timeout()
|
|
||||||
)
|
|
||||||
|
|
||||||
rx = sender.enqueue(
|
rx = sender.enqueue(
|
||||||
functions.invoke_with_layer(
|
functions.invoke_with_layer(
|
||||||
|
|
Loading…
Reference in New Issue
Block a user