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