mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-02-03 13:14:31 +03:00
Continue implementation and documentation
This commit is contained in:
parent
6a880f1ff1
commit
18895748c4
20
client/doc/_static/custom.css
vendored
Normal file
20
client/doc/_static/custom.css
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
.underline {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strikethrough {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler {
|
||||||
|
border-radius: 5%;
|
||||||
|
padding: 0.1em;
|
||||||
|
background-color: gray;
|
||||||
|
color: gray;
|
||||||
|
transition: all 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler:hover {
|
||||||
|
background-color: unset;
|
||||||
|
color: unset;
|
||||||
|
}
|
|
@ -56,8 +56,8 @@ Interactive login
|
||||||
The library offers a method for "quick and dirty" scripts known as :meth:`~Client.interactive_login`.
|
The library offers a method for "quick and dirty" scripts known as :meth:`~Client.interactive_login`.
|
||||||
This method will first check whether the account was previously logged-in, and if not, ask for a phone number to be input.
|
This method will first check whether the account was previously logged-in, and if not, ask for a phone number to be input.
|
||||||
|
|
||||||
You can write the code in a file (such as ``hello.py``) and then run it, or use the built-in ``asyncio``-enabled REPL.
|
You can write the code in a file (such as ``hello.py``) and then run it, or use the built-in :mod:`asyncio`-enabled REPL.
|
||||||
For this tutorial, we'll be using the ``asyncio`` REPL:
|
For this tutorial, we'll be using the :mod:`asyncio` REPL:
|
||||||
|
|
||||||
.. code-block:: shell
|
.. code-block:: shell
|
||||||
|
|
||||||
|
@ -209,3 +209,6 @@ Otherwise, you might run into errors such as tasks being destroyed while pending
|
||||||
Once a :class:`Client` instance has been connected, you cannot change the :mod:`asyncio` event loop.
|
Once a :class:`Client` instance has been connected, you cannot change the :mod:`asyncio` event loop.
|
||||||
Methods like :func:`asyncio.run` setup and tear-down a new event loop every time.
|
Methods like :func:`asyncio.run` setup and tear-down a new event loop every time.
|
||||||
If the loop changes, the client is likely to be "stuck" because its loop cannot advance.
|
If the loop changes, the client is likely to be "stuck" because its loop cannot advance.
|
||||||
|
|
||||||
|
If you want to learn how :mod:`asyncio` works under the hood or need an introduction,
|
||||||
|
you can read my own blog post `An Introduction to Asyncio <https://lonami.dev/blog/asyncio/>`_.
|
||||||
|
|
|
@ -44,6 +44,11 @@ Glossary
|
||||||
|
|
||||||
.. seealso:: The :doc:`../concepts/botapi-vs-mtproto` concept.
|
.. seealso:: The :doc:`../concepts/botapi-vs-mtproto` concept.
|
||||||
|
|
||||||
|
HTTP Bot API
|
||||||
|
Telegram's simplified HTTP API to control bot accounts only.
|
||||||
|
|
||||||
|
.. seealso:: The :doc:`../concepts/botapi-vs-mtproto` concept.
|
||||||
|
|
||||||
login
|
login
|
||||||
Used to refer to the login process as a whole, as opposed to the action to :term:`sign in`.
|
Used to refer to the login process as a whole, as opposed to the action to :term:`sign in`.
|
||||||
The "login code" or "login token" get their name because they belong to the login process.
|
The "login code" or "login token" get their name because they belong to the login process.
|
||||||
|
|
191
client/doc/concepts/messages.rst
Normal file
191
client/doc/concepts/messages.rst
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
Messages
|
||||||
|
========
|
||||||
|
|
||||||
|
.. currentmodule:: telethon
|
||||||
|
|
||||||
|
.. role:: underline
|
||||||
|
:class: underline
|
||||||
|
|
||||||
|
.. role:: strikethrough
|
||||||
|
:class: strikethrough
|
||||||
|
|
||||||
|
.. role:: spoiler
|
||||||
|
:class: spoiler
|
||||||
|
|
||||||
|
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 messages
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The library supports 3 formatting modes: no formatting, CommonMark, HTML.
|
||||||
|
|
||||||
|
Telegram does not natively support markdown or HTML.
|
||||||
|
Clients such as Telethon parse the text into a list of formatting :tl:`MessageEntity` at different offsets.
|
||||||
|
|
||||||
|
Note that `CommonMark's markdown <https://commonmark.org/>`_ is not fully compatible with :term:`HTTP Bot API`'s
|
||||||
|
`MarkdownV2 style <https://core.telegram.org/bots/api#markdownv2-style>`_, and does not support spoilers::
|
||||||
|
|
||||||
|
*italic* and _italic_
|
||||||
|
**bold** and __bold__
|
||||||
|
# headings are underlined
|
||||||
|
~~strikethrough~~
|
||||||
|
[inline URL](https://www.example.com/)
|
||||||
|
[inline mention](tg://user?id=ab1234cd6789)
|
||||||
|
custom emoji image with ![👍](tg://emoji?id=1234567890)
|
||||||
|
`inline code`
|
||||||
|
```python
|
||||||
|
multiline pre-formatted
|
||||||
|
block with optional language
|
||||||
|
```
|
||||||
|
|
||||||
|
HTML is also not fully compatible with :term:`HTTP Bot API`'s
|
||||||
|
`MarkdownV2 style <https://core.telegram.org/bots/api#markdownv2-style>`_,
|
||||||
|
and instead favours more standard `HTML elements <https://developer.mozilla.org/en-US/docs/Web/HTML/Element>`_:
|
||||||
|
|
||||||
|
* ``strong`` and ``b`` for **bold**.
|
||||||
|
* ``em`` and ``i`` for *italics*.
|
||||||
|
* ``u`` for :underline:`underlined text`.
|
||||||
|
* ``del`` and ``s`` for :strikethrough:`strikethrough`.
|
||||||
|
* ``blockquote`` for quotes.
|
||||||
|
* ``details`` for :spoiler:`hidden text` (spoiler).
|
||||||
|
* ``code`` for ``inline code``
|
||||||
|
* ``pre`` for multiple lines of code.
|
||||||
|
* ``a`` for links.
|
||||||
|
* ``img`` for inline images (only custom emoji).
|
||||||
|
|
||||||
|
Both markdown and HTML recognise the following special URLs using the ``tg:`` protocol:
|
||||||
|
|
||||||
|
* ``tg://user?id=ab1234cd6789`` for inline mentions.
|
||||||
|
To make sure the mention works, use :attr:`types.PackedChat.hex`.
|
||||||
|
You can also use :attr:`types.User.id`, but the mention will fail if the user is not in cache.
|
||||||
|
* ``tg://emoji?id=1234567890`` for custom emoji.
|
||||||
|
You must use the document identifier as the value.
|
||||||
|
The alt-text of the image **must** be a emoji such as 👍.
|
||||||
|
|
||||||
|
|
||||||
|
To obtain a message's text formatted, use :attr:`types.Message.text_markdown` or :attr:`types.Message.text_html`.
|
||||||
|
|
||||||
|
To send a message with formatted text, use the ``markdown`` or ``html`` parameters in :meth:`Client.send_message`.
|
||||||
|
|
||||||
|
When sending files, the format is appended to the name of the ``caption`` parameter, either ``caption_markdown`` or ``caption_html``.
|
||||||
|
|
||||||
|
|
||||||
|
Message identifiers
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
This is an in-depth explanation for how the :attr:`types.Message.id` works.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
You can safely skip this section if you're not interested.
|
||||||
|
|
||||||
|
Every account, whether it's an user account or bot account, has its own message counter.
|
||||||
|
This counter starts at 1, and is incremented by 1 every time a new message is received.
|
||||||
|
In private conversations or small groups, each account will receive a copy each message.
|
||||||
|
The message identifier will be based on the message counter of the receiving account.
|
||||||
|
|
||||||
|
In megagroups and broadcast channels, the message counter instead belongs to the channel itself.
|
||||||
|
It also starts at 1 and is incremented by 1 for every message sent to the group or channel.
|
||||||
|
This means every account will see the same message identifier for a given mesasge in a group or channel.
|
||||||
|
|
||||||
|
This design has the following implications:
|
||||||
|
|
||||||
|
* The message identifier alone is enough to uniquely identify a message only if it's not from a megagroup or channel.
|
||||||
|
This is why :class:`events.MessageDeleted` does not need to (and doesn't always) include chat information.
|
||||||
|
* Messages cannot be deleted for one-side only in megagroups or channels.
|
||||||
|
Because every account shares the same identifier for the message, it cannot be deleted only for some.
|
||||||
|
* Links to messages only work for everyone inside megagroups or channels.
|
||||||
|
In private conversations and small groups, each account will have their own counter, and the identifiers won't match.
|
||||||
|
|
||||||
|
Let's look at a concrete example.
|
||||||
|
|
||||||
|
* You are logged in as User-A.
|
||||||
|
* Both User-B and User-C are your mutual contacts.
|
||||||
|
* You have share a small group called Group-S with User-B.
|
||||||
|
* You also share a megagroup called Group-M with User-C.
|
||||||
|
|
||||||
|
.. graphviz::
|
||||||
|
:caption: Demo scenario
|
||||||
|
|
||||||
|
digraph scenario {
|
||||||
|
"User A" [shape=trapezium];
|
||||||
|
"User B" [shape=box];
|
||||||
|
"User C" [shape=box];
|
||||||
|
|
||||||
|
"User A" -> "User B";
|
||||||
|
"User A" -> "User C";
|
||||||
|
|
||||||
|
"Group-S" -> "User A";
|
||||||
|
"Group-S" -> "User B";
|
||||||
|
|
||||||
|
"Group-M" -> "User A";
|
||||||
|
"Group-M" -> "User C";
|
||||||
|
}
|
||||||
|
|
||||||
|
Every account and channel has just been created.
|
||||||
|
This means everyone has a message counter of one.
|
||||||
|
|
||||||
|
First, User-A will sent a welcome message to both User-B and User-C::
|
||||||
|
|
||||||
|
User-A → User-B: Hey, welcome!
|
||||||
|
User-A → User-C: ¡Bienvenido!
|
||||||
|
|
||||||
|
* For User-A, "Hey, welcome!" will have the message identifier 1. The message with "¡Bienvenido!" will have an ID of 2.
|
||||||
|
* For User-B, "Hey, welcome" will have ID 1.
|
||||||
|
* For User-B, "¡Bienvenido!" will have ID 1.
|
||||||
|
|
||||||
|
.. csv-table:: Message identifiers
|
||||||
|
:header: "Message", "User-A", "User-B", "User-C", "Group-S", "Group-M"
|
||||||
|
|
||||||
|
"Hey, welcome!", 1, 1, "", "", ""
|
||||||
|
"¡Bienvenido!", 2, "", 1, "", ""
|
||||||
|
|
||||||
|
Next, User-B and User-C will respond to User-A::
|
||||||
|
|
||||||
|
User-B → User-A: Thanks!
|
||||||
|
User-C → User-A: Gracias :)
|
||||||
|
|
||||||
|
.. csv-table:: Message identifiers
|
||||||
|
:header: "Message", "User-A", "User-B", "User-C", "Group-S", "Group-M"
|
||||||
|
|
||||||
|
"Hey, welcome!", 1, 1, "", "", ""
|
||||||
|
"¡Bienvenido!", 2, "", 1, "", ""
|
||||||
|
"Thanks!", 3, 2, "", "", ""
|
||||||
|
"Gracias :)", 4, "", 2, "", ""
|
||||||
|
|
||||||
|
Notice how for each message, the counter goes up by one, and they are independent.
|
||||||
|
|
||||||
|
Let's see what happens when User-B sends a message to Group-S::
|
||||||
|
|
||||||
|
User-B → Group-S: Nice group
|
||||||
|
|
||||||
|
.. csv-table:: Message identifiers
|
||||||
|
:header: "Message", "User-A", "User-B", "User-C", "Group-S", "Group-M"
|
||||||
|
|
||||||
|
"Hey, welcome!", 1, 1, "", "", ""
|
||||||
|
"¡Bienvenido!", 2, "", 1, "", ""
|
||||||
|
"Thanks!", 3, 2, "", "", ""
|
||||||
|
"Gracias :)", 4, "", 2, "", ""
|
||||||
|
"Nice group", 5, 3, "", "", ""
|
||||||
|
|
||||||
|
While the message was sent to a different chat, the group itself doesn't have a counter.
|
||||||
|
The message identifiers are still unique for each account.
|
||||||
|
The chat where the message was sent can be completely ignored.
|
||||||
|
|
||||||
|
Megagroups behave differently::
|
||||||
|
|
||||||
|
User-C → Group-M: Buen grupo
|
||||||
|
|
||||||
|
.. csv-table:: Message identifiers
|
||||||
|
:header: "Message", "User-A", "User-B", "User-C", "Group-S", "Group-M"
|
||||||
|
|
||||||
|
"Hey, welcome!", 1, 1, "", "", ""
|
||||||
|
"¡Bienvenido!", 2, "", 1, "", ""
|
||||||
|
"Thanks!", 3, 2, "", "", ""
|
||||||
|
"Gracias :)", 4, "", 2, "", ""
|
||||||
|
"Nice group", 5, 3, "", "", ""
|
||||||
|
"Buen grupo", "", "", "", "", 1
|
||||||
|
|
||||||
|
The group has its own message counter.
|
||||||
|
Each user won't get a copy of the message with their own identifier, but rather everyone sees the same message.
|
|
@ -40,3 +40,4 @@ graphviz_output_format = "svg"
|
||||||
|
|
||||||
html_theme = "sphinx_rtd_theme"
|
html_theme = "sphinx_rtd_theme"
|
||||||
html_static_path = ["_static"]
|
html_static_path = ["_static"]
|
||||||
|
html_css_files = ["custom.css"]
|
||||||
|
|
2
client/doc/developing/faq.rst
Normal file
2
client/doc/developing/faq.rst
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Frequently Asked Questions
|
||||||
|
==========================
|
|
@ -37,6 +37,7 @@ I can guarantee you will into far less problems.
|
||||||
|
|
||||||
Without further ado, let's take a look at the biggest changes.
|
Without further ado, let's take a look at the biggest changes.
|
||||||
This list may not be exhaustive, but it should give you an idea on what to expect.
|
This list may not be exhaustive, but it should give you an idea on what to expect.
|
||||||
|
If you feel like a major change is missing, please `open an issue <https://github.com/LonamiWebs/Telethon/>`_.
|
||||||
|
|
||||||
|
|
||||||
Complete project restructure
|
Complete project restructure
|
||||||
|
@ -53,6 +54,12 @@ The public modules under the ``telethon`` now make actual sense.
|
||||||
It's actually a factory object returning new error types on demand.
|
It's actually a factory object returning new error types on demand.
|
||||||
This means you don't need to wait for new library versions to be released to catch them.
|
This means you don't need to wait for new library versions to be released to catch them.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Be sure to check the documentation for :data:`telethon.errors` to learn about error changes.
|
||||||
|
Notably, errors such as ``FloodWaitError`` no longer have a ``.seconds`` field.
|
||||||
|
Instead, every value for every error type is always ``.value``.
|
||||||
|
|
||||||
This was also a good opportunity to remove a lot of modules that were not supposed to public in their entirety:
|
This was also a good opportunity to remove a lot of modules that were not supposed to public in their entirety:
|
||||||
``.crypto``, ``.extensions``, ``.network``, ``.custom``, ``.functions``, ``.helpers``, ``.hints``, ``.password``, ``.requestiter``, ``.sync``, ``.types``, ``.utils``.
|
``.crypto``, ``.extensions``, ``.network``, ``.custom``, ``.functions``, ``.helpers``, ``.hints``, ``.password``, ``.requestiter``, ``.sync``, ``.types``, ``.utils``.
|
||||||
|
|
||||||
|
@ -86,14 +93,49 @@ Each of them can have an additional namespace (as seen above with ``account.``).
|
||||||
|
|
||||||
* ``tl.functions`` contains every :term:`TL` definition treated as a function.
|
* ``tl.functions`` contains every :term:`TL` definition treated as a function.
|
||||||
The naming convention now follows Python's, and are ``snake_case``.
|
The naming convention now follows Python's, and are ``snake_case``.
|
||||||
They're no longer a class with attributes.
|
|
||||||
They serialize the request immediately.
|
|
||||||
* ``tl.abcs`` contains every abstract class, the "boxed" types from Telegram.
|
* ``tl.abcs`` contains every abstract class, the "boxed" types from Telegram.
|
||||||
You can use these for your type-hinting needs.
|
You can use these for your type-hinting needs.
|
||||||
* ``tl.types`` contains concrete instances, the "bare" types Telegram actually returns.
|
* ``tl.types`` contains concrete instances, the "bare" types Telegram actually returns.
|
||||||
You'll probably use these with :func:`isinstance` a lot.
|
You'll probably use these with :func:`isinstance` a lot.
|
||||||
All types use :term:`__slots__` to save space.
|
|
||||||
This means you can't add extra fields to these at runtime unless you subclass.
|
|
||||||
|
Raw API has a reduced feature-set
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
The string representation is now on :meth:`object.__repr__`, not :meth:`object.__str__`.
|
||||||
|
|
||||||
|
All types use :term:`__slots__` to save space.
|
||||||
|
This means you can't add extra fields to these at runtime unless you subclass.
|
||||||
|
|
||||||
|
The ``.stringify()`` methods on all TL types no longer exists.
|
||||||
|
Instead, you can use a library like `beauty-print <https://pypi.org/project/beauty-print/>`_.
|
||||||
|
|
||||||
|
The ``.to_dict()`` method on all TL types no longer exists.
|
||||||
|
The same is true for ``.to_json()``.
|
||||||
|
Instead, you can use a library like `json-pickle <https://pypi.org/project/jsonpickle/>`_ or write your own:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def to_dict(obj):
|
||||||
|
if obj is None or isinstance(obj, (bool, int, bytes, str)): return obj
|
||||||
|
if isinstance(obj, list): return [to_dict(x) for x in obj]
|
||||||
|
if isinstance(obj, dict): return {k: to_dict(v) for k, v in obj.items()}
|
||||||
|
return {slot: to_dict(getattr(obj, slot)) for slot in obj.__slots__}
|
||||||
|
|
||||||
|
Lesser-known methods such as ``TLObject.pretty_format``, ``serialize_bytes``, ``serialize_datetime`` and ``from_reader`` are also gone.
|
||||||
|
The remaining methods are:
|
||||||
|
|
||||||
|
* ``Serializable.constructor_id()`` class-method, to get the integer identifier of the corresponding type constructor.
|
||||||
|
* ``Serializable.from_bytes()`` class-method, to convert serialized :class:`bytes` back into the class.
|
||||||
|
* :meth:`object.__bytes__` instance-method, to serialize the instance into :class:`bytes` the way Telegram expects.
|
||||||
|
|
||||||
|
Functions are no longer a class with attributes.
|
||||||
|
They serialize the request immediately.
|
||||||
|
This means you cannot create request instance and change it later.
|
||||||
|
Consider using :func:`functools.partial` if you want to reuse parts of a request instead.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
|
||||||
Unified client iter and get methods
|
Unified client iter and get methods
|
||||||
|
@ -159,6 +201,66 @@ The simplest approach could be using a global ``states`` dictionary storing the
|
||||||
del states[event.sender.id]
|
del states[event.sender.id]
|
||||||
|
|
||||||
|
|
||||||
|
.. rubric:: No ``client.kick_participant()`` method.
|
||||||
|
|
||||||
|
This is not a thing in Telegram.
|
||||||
|
It was implemented by restricting and then removing the restriction.
|
||||||
|
|
||||||
|
The old ``client.edit_permissions()`` was renamed to :meth:`Client.set_banned_rights`.
|
||||||
|
This defines the rights a restricted participant has (bans them from doing other things).
|
||||||
|
Revoking the right to view messages will kick them.
|
||||||
|
This rename should avoid confusion, as it is now clear this is not to promote users to admin status.
|
||||||
|
|
||||||
|
For administrators, ``client.edit_admin`` was renamed to :meth:`Client.set_admin_rights` for consistency.
|
||||||
|
|
||||||
|
Note that a new method, :meth:`Client.set_default_rights`, must now be used to set a chat's default rights.
|
||||||
|
|
||||||
|
.. rubric:: No ``client.download_profile_photo()`` method.
|
||||||
|
|
||||||
|
You can simply use :meth:`Client.download` now.
|
||||||
|
Note that :meth:`~Client.download` no longer supports downloading contacts as ``.vcard``.
|
||||||
|
|
||||||
|
.. rubric:: No ``client.set_proxy()`` method.
|
||||||
|
|
||||||
|
Proxy support is no longer built-in.
|
||||||
|
They were never officially maintained.
|
||||||
|
This doesn't mean you can't use them.
|
||||||
|
You're now free to choose your own proxy library and pass a different connector to the :class:`Client` constructor.
|
||||||
|
|
||||||
|
This should hopefully make it clear that most connection issues when using proxies do *not* come from Telethon.
|
||||||
|
|
||||||
|
.. rubric:: No ``client.set_receive_updates`` method.
|
||||||
|
|
||||||
|
It was not working as expected.
|
||||||
|
|
||||||
|
.. rubric:: No ``client.catch_up()`` method.
|
||||||
|
|
||||||
|
You can still configure it when creating the :class:`Client`, which was the only way to make it work anyway.
|
||||||
|
|
||||||
|
.. rubric:: No ``client.action()`` method.
|
||||||
|
|
||||||
|
.. rubric:: No ``client.takeout()`` method.
|
||||||
|
|
||||||
|
.. rubric:: No ``client.qr_login()`` method.
|
||||||
|
|
||||||
|
.. rubric:: No ``client.edit_2fa()`` method.
|
||||||
|
|
||||||
|
.. rubric:: No ``client.get_stats()`` method.
|
||||||
|
|
||||||
|
.. rubric:: No ``client.edit_folder()`` method.
|
||||||
|
|
||||||
|
.. rubric:: No ``client.build_reply_markup()`` method.
|
||||||
|
|
||||||
|
.. rubric:: No ``client.list_event_handlers()`` method.
|
||||||
|
|
||||||
|
These are out of scope for the time being.
|
||||||
|
They might be re-introduced in the future if there is a burning need for them and are not difficult to maintain.
|
||||||
|
This doesn't mean you can't do these things anymore though, since the :term:`Raw API` is still available.
|
||||||
|
|
||||||
|
Telethon v2 is committed to not exposing the raw API under any public API of the ``telethon`` package.
|
||||||
|
This means any method returning data from Telegram must have a custom wrapper object and be maintained too.
|
||||||
|
Because the standards are higher, the barrier of entry for new additions and features is higher too.
|
||||||
|
|
||||||
|
|
||||||
No message.raw_text or message.message
|
No message.raw_text or message.message
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
@ -201,6 +303,57 @@ This means getting those identifiers is up to you, and you can handle it in a wa
|
||||||
In-depth explanation for :doc:`/concepts/updates`.
|
In-depth explanation for :doc:`/concepts/updates`.
|
||||||
|
|
||||||
|
|
||||||
|
Behaviour changes in events
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
:class:`events.CallbackQuery` no longer also handles "inline bot callback queries".
|
||||||
|
This was a hacky workaround.
|
||||||
|
|
||||||
|
:class:`events.MessageRead` no longer triggers when the *contents* of a message are read, such as voice notes being played.
|
||||||
|
|
||||||
|
Albums in Telegram are an illusion.
|
||||||
|
There is no "album media".
|
||||||
|
There is only separate messages pretending to be a single message.
|
||||||
|
|
||||||
|
``events.Album`` was a hack that waited for a small amount of time to group messages sharing the same grouped identifier.
|
||||||
|
If you want to wait for a full album, you will need to wait yourself:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
pending_albums = {} # global for simplicity
|
||||||
|
async def gather_album(event, handler):
|
||||||
|
if pending := pending_albums.get(event.grouped_id):
|
||||||
|
pending.append(event)
|
||||||
|
else:
|
||||||
|
pending_albums[event.grouped_id] = [event]
|
||||||
|
# Wait for other events to come in. Adjust delay to your needs.
|
||||||
|
# This will NOT work if sequential updates are enabled (spawn a task to do the rest instead).
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
events = pending_albums.pop(grouped_id, [])
|
||||||
|
await handler(events)
|
||||||
|
|
||||||
|
@client.on(events.NewMessage)
|
||||||
|
async def handler(event):
|
||||||
|
if event.grouped_id:
|
||||||
|
await gather_album(event, handle_album)
|
||||||
|
else:
|
||||||
|
await handle_message(event)
|
||||||
|
|
||||||
|
async def handle_album(events):
|
||||||
|
... # do stuff with events
|
||||||
|
|
||||||
|
async def handle_message(event):
|
||||||
|
... # do stuff with event
|
||||||
|
|
||||||
|
Note that the above code is not foolproof and will not handle more than one client.
|
||||||
|
It might be possible for album events to be delayed for more than a second.
|
||||||
|
|
||||||
|
Note that messages that do **not** belong to an album can be received in-between an album.
|
||||||
|
|
||||||
|
Overall, it's probably better if you treat albums for what they really are:
|
||||||
|
separate messages sharing a :attr:`~types.Message.grouped_id`.
|
||||||
|
|
||||||
|
|
||||||
Streamlined chat, input_chat and chat_id
|
Streamlined chat, input_chat and chat_id
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
|
||||||
|
@ -232,6 +385,13 @@ Overall, dealing with users, groups and channels should feel a lot more natural.
|
||||||
In-depth explanation for :doc:`/concepts/chats`.
|
In-depth explanation for :doc:`/concepts/chats`.
|
||||||
|
|
||||||
|
|
||||||
|
Other methods like ``client.get_peer_id``, ``client.get_input_entity`` and ``client.get_entity`` are gone too.
|
||||||
|
While not directly related, ``client.is_bot`` is gone as well.
|
||||||
|
You can use :meth:`Client.get_me` or read it from the session instead.
|
||||||
|
|
||||||
|
The ``telethon.utils`` package is gone entirely, so methods like ``utils.resolve_id`` no longer exist either.
|
||||||
|
|
||||||
|
|
||||||
Session cache no longer exists
|
Session cache no longer exists
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
|
@ -264,7 +424,7 @@ TelegramClient renamed to Client
|
||||||
You can rename it with :keyword:`as` during import if you want to use the old name.
|
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.
|
Python allows using namespaces via packages and modules.
|
||||||
Therefore, the full name :class:`telethon.Client` already indicates it's from ``telethon``, so the old name was redundant.
|
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
|
Changes to start and client context-manager
|
||||||
|
@ -283,8 +443,9 @@ 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.
|
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.
|
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 ``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
|
No telethon.sync hack
|
||||||
---------------------
|
---------------------
|
||||||
|
|
|
@ -41,6 +41,30 @@ The implementation consists of a parser and a code generator.
|
||||||
The parser is able to read parse ``.tl`` files (:term:`Type Language` definition files).
|
The parser is able to read parse ``.tl`` files (:term:`Type Language` definition files).
|
||||||
It doesn't do anything with the files other than to represent the content as Python objects.
|
It doesn't do anything with the files other than to represent the content as Python objects.
|
||||||
|
|
||||||
|
.. admonition:: Type Language brief
|
||||||
|
|
||||||
|
TL-definitions are statements terminated with a semicolon ``;`` and often defined in a single line:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
geoPointEmpty#1117dd5f = GeoPoint;
|
||||||
|
geoPoint#b2a2f663 flags:# long:double lat:double access_hash:long accuracy_radius:flags.0?int = GeoPoint;
|
||||||
|
|
||||||
|
The first word is the name, optionally followed by the hash sign ``#`` and an hexadecimal number.
|
||||||
|
Every definition can have a constructor identifier inferred based on its own text representation.
|
||||||
|
The hexadecimal number will override the constructor identifier used for the definition.
|
||||||
|
|
||||||
|
What follows up to the equals-sign ``=`` are the fields of the definition.
|
||||||
|
They have a name and a type, separated by the colon ``:``.
|
||||||
|
|
||||||
|
The type ``#`` represents a bitflag.
|
||||||
|
Other fields can be conditionally serialized by prefixing the type with ``flag_name.bit_index?``.
|
||||||
|
|
||||||
|
After the equal-sign comes the name for the "base class".
|
||||||
|
This representation is known as "boxed", and it contains the constructor identifier to discriminate a definition.
|
||||||
|
If the definition name appears on its own, it will be "bare" and will not have the constructor identifier prefix.
|
||||||
|
|
||||||
|
|
||||||
The code generator uses the parsed definitions to generate Python code.
|
The code generator uses the parsed definitions to generate Python code.
|
||||||
Most of the code to serialize and deserialize objects lives under ``serde/``.
|
Most of the code to serialize and deserialize objects lives under ``serde/``.
|
||||||
|
|
||||||
|
|
|
@ -102,14 +102,15 @@ A more in-depth explanation of some of the concepts and words used in Telethon.
|
||||||
|
|
||||||
concepts/chats
|
concepts/chats
|
||||||
concepts/updates
|
concepts/updates
|
||||||
|
concepts/messages
|
||||||
concepts/sessions
|
concepts/sessions
|
||||||
concepts/errors
|
concepts/errors
|
||||||
concepts/botapi-vs-mtproto
|
concepts/botapi-vs-mtproto
|
||||||
concepts/full-api
|
concepts/full-api
|
||||||
concepts/glossary
|
concepts/glossary
|
||||||
|
|
||||||
Developing
|
Development resources
|
||||||
==========
|
=====================
|
||||||
|
|
||||||
Tips and tricks to develop both with the library and for the library.
|
Tips and tricks to develop both with the library and for the library.
|
||||||
|
|
||||||
|
@ -117,10 +118,11 @@ Tips and tricks to develop both with the library and for the library.
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:hidden:
|
:hidden:
|
||||||
:caption: Developing
|
:caption: Development resources
|
||||||
|
|
||||||
developing/changelog
|
developing/changelog
|
||||||
developing/migration-guide
|
developing/migration-guide
|
||||||
|
developing/faq
|
||||||
developing/philosophy.rst
|
developing/philosophy.rst
|
||||||
developing/coding-style.rst
|
developing/coding-style.rst
|
||||||
developing/project-structure.rst
|
developing/project-structure.rst
|
||||||
|
|
|
@ -31,6 +31,7 @@ Errors
|
||||||
except errors.FloodWait as e:
|
except errors.FloodWait as e:
|
||||||
await asyncio.sleep(e.value)
|
await asyncio.sleep(e.value)
|
||||||
|
|
||||||
|
Note how the :attr:`RpcError.value` field is still accessible, as it's a subclass of :class:`RpcError`.
|
||||||
The code above is equivalent to the following:
|
The code above is equivalent to the following:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
|
@ -3,8 +3,9 @@ from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from ...tl import abcs, functions, types
|
from ...tl import abcs, functions, types
|
||||||
from ..types import AsyncList, ChatLike, Participant
|
from ..types import AsyncList, ChatLike, File, Participant, RecentAction
|
||||||
from ..utils import build_chat_map
|
from ..utils import build_chat_map
|
||||||
|
from .messages import SearchList
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
@ -81,3 +82,106 @@ class ParticipantList(AsyncList[Participant]):
|
||||||
|
|
||||||
def get_participants(self: Client, chat: ChatLike) -> AsyncList[Participant]:
|
def get_participants(self: Client, chat: ChatLike) -> AsyncList[Participant]:
|
||||||
return ParticipantList(self, chat)
|
return ParticipantList(self, chat)
|
||||||
|
|
||||||
|
|
||||||
|
class RecentActionList(AsyncList[RecentAction]):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client: Client,
|
||||||
|
chat: ChatLike,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self._client = client
|
||||||
|
self._chat = chat
|
||||||
|
self._peer: Optional[types.InputChannel] = None
|
||||||
|
self._offset = 0
|
||||||
|
|
||||||
|
async def _fetch_next(self) -> None:
|
||||||
|
if self._peer is None:
|
||||||
|
self._peer = (
|
||||||
|
await self._client._resolve_to_packed(self._chat)
|
||||||
|
)._to_input_channel()
|
||||||
|
|
||||||
|
result = await self._client(
|
||||||
|
functions.channels.get_admin_log(
|
||||||
|
channel=self._peer,
|
||||||
|
q="",
|
||||||
|
min_id=0,
|
||||||
|
max_id=self._offset,
|
||||||
|
limit=100,
|
||||||
|
events_filter=None,
|
||||||
|
admins=[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert isinstance(result, types.channels.AdminLogResults)
|
||||||
|
|
||||||
|
chat_map = build_chat_map(result.users, result.chats)
|
||||||
|
self._buffer.extend(RecentAction._create(e, chat_map) for e in result.events)
|
||||||
|
self._total += len(self._buffer)
|
||||||
|
|
||||||
|
if self._buffer:
|
||||||
|
self._offset = min(e.id for e in self._buffer)
|
||||||
|
|
||||||
|
|
||||||
|
def get_admin_log(self: Client, chat: ChatLike) -> AsyncList[RecentAction]:
|
||||||
|
return RecentActionList(self, chat)
|
||||||
|
|
||||||
|
|
||||||
|
class ProfilePhotoList(AsyncList[File]):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client: Client,
|
||||||
|
chat: ChatLike,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self._client = client
|
||||||
|
self._chat = chat
|
||||||
|
self._peer: Optional[abcs.InputPeer] = None
|
||||||
|
self._search_iter: Optional[SearchList] = None
|
||||||
|
|
||||||
|
async def _fetch_next(self) -> None:
|
||||||
|
if self._peer is None:
|
||||||
|
self._peer = (
|
||||||
|
await self._client._resolve_to_packed(self._chat)
|
||||||
|
)._to_input_peer()
|
||||||
|
|
||||||
|
if isinstance(self._peer, types.InputPeerUser):
|
||||||
|
result = await self._client(
|
||||||
|
functions.photos.get_user_photos(
|
||||||
|
user_id=types.InputUser(
|
||||||
|
user_id=self._peer.user_id, access_hash=self._peer.access_hash
|
||||||
|
),
|
||||||
|
offset=0,
|
||||||
|
max_id=0,
|
||||||
|
limit=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(result, types.photos.Photos):
|
||||||
|
self._buffer.extend(
|
||||||
|
filter(None, (File._try_from_raw_photo(p) for p in result.photos))
|
||||||
|
)
|
||||||
|
self._total = len(result.photos)
|
||||||
|
elif isinstance(result, types.photos.PhotosSlice):
|
||||||
|
self._buffer.extend(
|
||||||
|
filter(None, (File._try_from_raw_photo(p) for p in result.photos))
|
||||||
|
)
|
||||||
|
self._total = result.count
|
||||||
|
else:
|
||||||
|
raise RuntimeError("unexpected case")
|
||||||
|
|
||||||
|
|
||||||
|
def get_profile_photos(self: Client, chat: ChatLike) -> AsyncList[File]:
|
||||||
|
return ProfilePhotoList(self, chat)
|
||||||
|
|
||||||
|
|
||||||
|
def set_banned_rights(self: Client, chat: ChatLike, user: ChatLike) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def set_admin_rights(self: Client, chat: ChatLike, user: ChatLike) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def set_default_rights(self: Client, chat: ChatLike, user: ChatLike) -> None:
|
||||||
|
pass
|
||||||
|
|
|
@ -36,6 +36,7 @@ from ..types import (
|
||||||
Chat,
|
Chat,
|
||||||
ChatLike,
|
ChatLike,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
Draft,
|
||||||
File,
|
File,
|
||||||
InFileLike,
|
InFileLike,
|
||||||
LoginToken,
|
LoginToken,
|
||||||
|
@ -43,6 +44,7 @@ from ..types import (
|
||||||
OutFileLike,
|
OutFileLike,
|
||||||
Participant,
|
Participant,
|
||||||
PasswordToken,
|
PasswordToken,
|
||||||
|
RecentAction,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from .auth import (
|
from .auth import (
|
||||||
|
@ -55,8 +57,15 @@ from .auth import (
|
||||||
sign_out,
|
sign_out,
|
||||||
)
|
)
|
||||||
from .bots import InlineResult, inline_query
|
from .bots import InlineResult, inline_query
|
||||||
from .chats import get_participants
|
from .chats import (
|
||||||
from .dialogs import delete_dialog, get_dialogs
|
get_admin_log,
|
||||||
|
get_participants,
|
||||||
|
get_profile_photos,
|
||||||
|
set_admin_rights,
|
||||||
|
set_banned_rights,
|
||||||
|
set_default_rights,
|
||||||
|
)
|
||||||
|
from .dialogs import delete_dialog, get_dialogs, get_drafts
|
||||||
from .files import (
|
from .files import (
|
||||||
download,
|
download,
|
||||||
get_file_bytes,
|
get_file_bytes,
|
||||||
|
@ -198,7 +207,7 @@ class Client:
|
||||||
if self._config.catch_up and self._config.session.state:
|
if self._config.catch_up and self._config.session.state:
|
||||||
self._message_box.load(self._config.session.state)
|
self._message_box.load(self._config.session.state)
|
||||||
|
|
||||||
# ---
|
# Begin partially @generated
|
||||||
|
|
||||||
def add_event_handler(
|
def add_event_handler(
|
||||||
self,
|
self,
|
||||||
|
@ -511,6 +520,29 @@ class Client:
|
||||||
"""
|
"""
|
||||||
return await forward_messages(self, target, message_ids, source)
|
return await forward_messages(self, target, message_ids, source)
|
||||||
|
|
||||||
|
def get_admin_log(self, chat: ChatLike) -> AsyncList[RecentAction]:
|
||||||
|
"""
|
||||||
|
Get the recent actions from the administrator's log.
|
||||||
|
|
||||||
|
This method requires you to be an administrator in the :term:`chat`.
|
||||||
|
|
||||||
|
The returned actions are also known as "admin log events".
|
||||||
|
|
||||||
|
:param chat:
|
||||||
|
The :term:`chat` to fetch recent actions from.
|
||||||
|
|
||||||
|
:return: The recent actions.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async for admin_log_event in client.get_admin_log(chat):
|
||||||
|
if message := admin_log_event.deleted_message:
|
||||||
|
print('Deleted:', message.text)
|
||||||
|
"""
|
||||||
|
return get_admin_log(self, chat)
|
||||||
|
|
||||||
def get_contacts(self) -> AsyncList[User]:
|
def get_contacts(self) -> AsyncList[User]:
|
||||||
"""
|
"""
|
||||||
Get the users in your contact list.
|
Get the users in your contact list.
|
||||||
|
@ -548,6 +580,47 @@ class Client:
|
||||||
"""
|
"""
|
||||||
return get_dialogs(self)
|
return get_dialogs(self)
|
||||||
|
|
||||||
|
def get_drafts(self) -> AsyncList[Draft]:
|
||||||
|
"""
|
||||||
|
Get all message drafts saved in any dialog.
|
||||||
|
|
||||||
|
:return: The existing message drafts.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async for draft in client.get_drafts():
|
||||||
|
await draft.delete()
|
||||||
|
"""
|
||||||
|
return get_drafts(self)
|
||||||
|
|
||||||
|
def get_file_bytes(self, media: File) -> AsyncList[bytes]:
|
||||||
|
"""
|
||||||
|
Get the contents of an uploaded media file as chunks of :class:`bytes`.
|
||||||
|
|
||||||
|
This lets you iterate over the chunks of a file and print progress while the download occurs.
|
||||||
|
|
||||||
|
If you just want to download a file to disk without printing progress, use :meth:`download` instead.
|
||||||
|
|
||||||
|
:param media:
|
||||||
|
The media file to download.
|
||||||
|
This will often come from :attr:`telethon.types.Message.file`.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
if file := message.file:
|
||||||
|
with open(f'media{file.ext}', 'wb') as fd:
|
||||||
|
downloaded = 0
|
||||||
|
async for chunk in client.get_file_bytes(file):
|
||||||
|
downloaded += len(chunk)
|
||||||
|
fd.write(chunk)
|
||||||
|
print(f'Downloaded {downloaded // 1024}/{file.size // 1024} KiB')
|
||||||
|
"""
|
||||||
|
return get_file_bytes(self, media)
|
||||||
|
|
||||||
def get_handler_filter(
|
def get_handler_filter(
|
||||||
self, handler: Callable[[Event], Awaitable[Any]]
|
self, handler: Callable[[Event], Awaitable[Any]]
|
||||||
) -> Optional[Filter]:
|
) -> Optional[Filter]:
|
||||||
|
@ -666,6 +739,23 @@ class Client:
|
||||||
"""
|
"""
|
||||||
return get_participants(self, chat)
|
return get_participants(self, chat)
|
||||||
|
|
||||||
|
def get_profile_photos(self, chat: ChatLike) -> AsyncList[File]:
|
||||||
|
"""
|
||||||
|
Get the profile pictures set in a chat, or user avatars.
|
||||||
|
|
||||||
|
:return: The photo files.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
async for photo in client.get_profile_photos(chat):
|
||||||
|
await client.download(photo, f'{i}.jpg')
|
||||||
|
i += 1
|
||||||
|
"""
|
||||||
|
return get_profile_photos(self, chat)
|
||||||
|
|
||||||
async def inline_query(
|
async def inline_query(
|
||||||
self, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None
|
self, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None
|
||||||
) -> AsyncIterator[InlineResult]:
|
) -> AsyncIterator[InlineResult]:
|
||||||
|
@ -730,34 +820,15 @@ class Client:
|
||||||
Check whether the client instance is authorized (i.e. logged-in).
|
Check whether the client instance is authorized (i.e. logged-in).
|
||||||
|
|
||||||
:return: :data:`True` if the client instance has signed-in.
|
:return: :data:`True` if the client instance has signed-in.
|
||||||
"""
|
|
||||||
return await is_authorized(self)
|
|
||||||
|
|
||||||
def get_file_bytes(self, media: File) -> AsyncList[bytes]:
|
|
||||||
"""
|
|
||||||
Get the contents of an uploaded media file as chunks of :class:`bytes`.
|
|
||||||
|
|
||||||
This lets you iterate over the chunks of a file and print progress while the download occurs.
|
|
||||||
|
|
||||||
If you just want to download a file to disk without printing progress, use :meth:`download` instead.
|
|
||||||
|
|
||||||
:param media:
|
|
||||||
The media file to download.
|
|
||||||
This will often come from :attr:`telethon.types.Message.file`.
|
|
||||||
|
|
||||||
.. rubric:: Example
|
.. rubric:: Example
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
if file := message.file:
|
if not await client.is_authorized():
|
||||||
with open(f'media{file.ext}', 'wb') as fd:
|
... # need to sign in
|
||||||
downloaded = 0
|
|
||||||
async for chunk in client.get_file_bytes(file):
|
|
||||||
downloaded += len(chunk)
|
|
||||||
fd.write(chunk)
|
|
||||||
print(f'Downloaded {downloaded // 1024}/{file.size // 1024} KiB')
|
|
||||||
"""
|
"""
|
||||||
return get_file_bytes(self, media)
|
return await is_authorized(self)
|
||||||
|
|
||||||
def on(
|
def on(
|
||||||
self, event_cls: Type[Event], filter: Optional[Filter] = None
|
self, event_cls: Type[Event], filter: Optional[Filter] = None
|
||||||
|
@ -934,6 +1005,13 @@ class Client:
|
||||||
This means only messages sent before *offset_date* will be fetched.
|
This means only messages sent before *offset_date* will be fetched.
|
||||||
|
|
||||||
:return: The found messages.
|
:return: The found messages.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async for message in client.search_all_messages(query='hello'):
|
||||||
|
print(message.text)
|
||||||
"""
|
"""
|
||||||
return search_all_messages(
|
return search_all_messages(
|
||||||
self, limit, query=query, offset_id=offset_id, offset_date=offset_date
|
self, limit, query=query, offset_id=offset_id, offset_date=offset_date
|
||||||
|
@ -970,6 +1048,13 @@ class Client:
|
||||||
This means only messages sent before *offset_date* will be fetched.
|
This means only messages sent before *offset_date* will be fetched.
|
||||||
|
|
||||||
:return: The found messages.
|
:return: The found messages.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async for message in client.search_messages(chat, query='hello'):
|
||||||
|
print(message.text)
|
||||||
"""
|
"""
|
||||||
return search_messages(
|
return search_messages(
|
||||||
self, chat, limit, query=query, offset_id=offset_id, offset_date=offset_date
|
self, chat, limit, query=query, offset_id=offset_id, offset_date=offset_date
|
||||||
|
@ -988,6 +1073,9 @@ class Client:
|
||||||
voice: bool = False,
|
voice: bool = False,
|
||||||
title: Optional[str] = None,
|
title: Optional[str] = None,
|
||||||
performer: Optional[str] = None,
|
performer: Optional[str] = None,
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
caption_markdown: Optional[str] = None,
|
||||||
|
caption_html: Optional[str] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Send an audio file.
|
Send an audio file.
|
||||||
|
@ -1002,6 +1090,12 @@ class Client:
|
||||||
A local file path or :class:`~telethon.types.File` to send.
|
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`.
|
The rest of parameters behave the same as they do in :meth:`send_file`.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
await client.send_audio(chat, 'file.ogg', voice=True)
|
||||||
"""
|
"""
|
||||||
return await send_audio(
|
return await send_audio(
|
||||||
self,
|
self,
|
||||||
|
@ -1015,6 +1109,9 @@ class Client:
|
||||||
voice=voice,
|
voice=voice,
|
||||||
title=title,
|
title=title,
|
||||||
performer=performer,
|
performer=performer,
|
||||||
|
caption=caption,
|
||||||
|
caption_markdown=caption_markdown,
|
||||||
|
caption_html=caption_html,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def send_file(
|
async def send_file(
|
||||||
|
@ -1072,7 +1169,16 @@ class Client:
|
||||||
if *path* isn't a :class:`~telethon.types.File`.
|
if *path* isn't a :class:`~telethon.types.File`.
|
||||||
See the documentation of :meth:`~telethon.types.File.new` to learn what they do.
|
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.
|
||||||
|
|
||||||
Note that only one *caption* parameter can be provided.
|
Note that only one *caption* parameter can be provided.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
login_token = await client.request_login_code('+1 23 456...')
|
||||||
|
print(login_token.timeout, 'seconds before code expires')
|
||||||
"""
|
"""
|
||||||
return await send_file(
|
return await send_file(
|
||||||
self,
|
self,
|
||||||
|
@ -1120,20 +1226,23 @@ class Client:
|
||||||
Message text, with no formatting.
|
Message text, with no formatting.
|
||||||
|
|
||||||
:param text_markdown:
|
:param text_markdown:
|
||||||
Message text, parsed as markdown.
|
Message text, parsed as CommonMark.
|
||||||
|
|
||||||
:param text_html:
|
:param text_html:
|
||||||
Message text, parsed as HTML.
|
Message text, parsed as HTML.
|
||||||
|
|
||||||
Note that exactly one *text* parameter must be provided.
|
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
|
||||||
|
|
||||||
|
await client.send_message(chat, markdown='**Hello!**')
|
||||||
"""
|
"""
|
||||||
return await send_message(
|
return await send_message(
|
||||||
self,
|
self, chat, text, markdown=markdown, html=html, link_preview=link_preview
|
||||||
chat,
|
|
||||||
text,
|
|
||||||
markdown=markdown,
|
|
||||||
html=html,
|
|
||||||
link_preview=link_preview,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def send_photo(
|
async def send_photo(
|
||||||
|
@ -1148,6 +1257,9 @@ class Client:
|
||||||
compress: bool = True,
|
compress: bool = True,
|
||||||
width: Optional[int] = None,
|
width: Optional[int] = None,
|
||||||
height: Optional[int] = None,
|
height: Optional[int] = None,
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
caption_markdown: Optional[str] = None,
|
||||||
|
caption_html: Optional[str] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Send a photo file.
|
Send a photo file.
|
||||||
|
@ -1166,6 +1278,12 @@ class Client:
|
||||||
A local file path or :class:`~telethon.types.File` to send.
|
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`.
|
The rest of parameters behave the same as they do in :meth:`send_file`.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
await client.send_photo(chat, 'photo.jpg', caption='Check this out!')
|
||||||
"""
|
"""
|
||||||
return await send_photo(
|
return await send_photo(
|
||||||
self,
|
self,
|
||||||
|
@ -1178,6 +1296,9 @@ class Client:
|
||||||
compress=compress,
|
compress=compress,
|
||||||
width=width,
|
width=width,
|
||||||
height=height,
|
height=height,
|
||||||
|
caption=caption,
|
||||||
|
caption_markdown=caption_markdown,
|
||||||
|
caption_html=caption_html,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def send_video(
|
async def send_video(
|
||||||
|
@ -1194,6 +1315,9 @@ class Client:
|
||||||
height: Optional[int] = None,
|
height: Optional[int] = None,
|
||||||
round: bool = False,
|
round: bool = False,
|
||||||
supports_streaming: bool = False,
|
supports_streaming: bool = False,
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
caption_markdown: Optional[str] = None,
|
||||||
|
caption_html: Optional[str] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Send a video file.
|
Send a video file.
|
||||||
|
@ -1208,6 +1332,12 @@ class Client:
|
||||||
A local file path or :class:`~telethon.types.File` to send.
|
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`.
|
The rest of parameters behave the same as they do in :meth:`send_file`.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
await client.send_video(chat, 'video.mp4', caption_markdown='*I cannot believe this just happened*')
|
||||||
"""
|
"""
|
||||||
return await send_video(
|
return await send_video(
|
||||||
self,
|
self,
|
||||||
|
@ -1222,8 +1352,20 @@ class Client:
|
||||||
height=height,
|
height=height,
|
||||||
round=round,
|
round=round,
|
||||||
supports_streaming=supports_streaming,
|
supports_streaming=supports_streaming,
|
||||||
|
caption=caption,
|
||||||
|
caption_markdown=caption_markdown,
|
||||||
|
caption_html=caption_html,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_admin_rights(self, chat: ChatLike, user: ChatLike) -> None:
|
||||||
|
set_admin_rights(self, chat, user)
|
||||||
|
|
||||||
|
def set_banned_rights(self, chat: ChatLike, user: ChatLike) -> None:
|
||||||
|
set_banned_rights(self, chat, user)
|
||||||
|
|
||||||
|
def set_default_rights(self, chat: ChatLike, user: ChatLike) -> None:
|
||||||
|
set_default_rights(self, chat, user)
|
||||||
|
|
||||||
def set_handler_filter(
|
def set_handler_filter(
|
||||||
self,
|
self,
|
||||||
handler: Callable[[Event], Awaitable[Any]],
|
handler: Callable[[Event], Awaitable[Any]],
|
||||||
|
@ -1314,7 +1456,7 @@ class Client:
|
||||||
"""
|
"""
|
||||||
await unpin_message(self, chat, message_id)
|
await unpin_message(self, chat, message_id)
|
||||||
|
|
||||||
# ---
|
# End partially @generated
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def connected(self) -> bool:
|
def connected(self) -> bool:
|
||||||
|
|
|
@ -2,8 +2,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ...tl import abcs, functions, types
|
from ...tl import functions, types
|
||||||
from ..types import AsyncList, ChatLike, Dialog, User
|
from ..types import AsyncList, ChatLike, Dialog, Draft
|
||||||
from ..utils import build_chat_map
|
from ..utils import build_chat_map
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -78,3 +78,29 @@ async def delete_dialog(self: Client, chat: ChatLike) -> None:
|
||||||
max_date=None,
|
max_date=None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DraftList(AsyncList[Draft]):
|
||||||
|
def __init__(self, client: Client):
|
||||||
|
super().__init__()
|
||||||
|
self._client = client
|
||||||
|
self._offset = 0
|
||||||
|
|
||||||
|
async def _fetch_next(self) -> None:
|
||||||
|
result = await self._client(functions.messages.get_all_drafts())
|
||||||
|
assert isinstance(result, types.Updates)
|
||||||
|
|
||||||
|
chat_map = build_chat_map(result.users, result.chats)
|
||||||
|
|
||||||
|
self._buffer.extend(
|
||||||
|
Draft._from_raw(u, chat_map)
|
||||||
|
for u in result.updates
|
||||||
|
if isinstance(u, types.UpdateDraftMessage)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._total = len(result.updates)
|
||||||
|
self._done = True
|
||||||
|
|
||||||
|
|
||||||
|
def get_drafts(self: Client) -> AsyncList[Draft]:
|
||||||
|
return DraftList(self)
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import hashlib
|
import hashlib
|
||||||
from functools import partial
|
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from io import BufferedWriter
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Coroutine, Optional, Tuple, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
from ...tl import abcs, functions, types
|
from ...tl import abcs, functions, types
|
||||||
from ..types import (
|
from ..types import (
|
||||||
|
@ -43,6 +40,9 @@ async def send_photo(
|
||||||
compress: bool = True,
|
compress: bool = True,
|
||||||
width: Optional[int] = None,
|
width: Optional[int] = None,
|
||||||
height: Optional[int] = None,
|
height: Optional[int] = None,
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
caption_markdown: Optional[str] = None,
|
||||||
|
caption_html: Optional[str] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
return await send_file(
|
return await send_file(
|
||||||
self,
|
self,
|
||||||
|
@ -55,6 +55,9 @@ async def send_photo(
|
||||||
compress=compress,
|
compress=compress,
|
||||||
width=width,
|
width=width,
|
||||||
height=height,
|
height=height,
|
||||||
|
caption=caption,
|
||||||
|
caption_markdown=caption_markdown,
|
||||||
|
caption_html=caption_html,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -71,6 +74,9 @@ async def send_audio(
|
||||||
voice: bool = False,
|
voice: bool = False,
|
||||||
title: Optional[str] = None,
|
title: Optional[str] = None,
|
||||||
performer: Optional[str] = None,
|
performer: Optional[str] = None,
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
caption_markdown: Optional[str] = None,
|
||||||
|
caption_html: Optional[str] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
return await send_file(
|
return await send_file(
|
||||||
self,
|
self,
|
||||||
|
@ -84,6 +90,9 @@ async def send_audio(
|
||||||
voice=voice,
|
voice=voice,
|
||||||
title=title,
|
title=title,
|
||||||
performer=performer,
|
performer=performer,
|
||||||
|
caption=caption,
|
||||||
|
caption_markdown=caption_markdown,
|
||||||
|
caption_html=caption_html,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,6 +110,9 @@ async def send_video(
|
||||||
height: Optional[int] = None,
|
height: Optional[int] = None,
|
||||||
round: bool = False,
|
round: bool = False,
|
||||||
supports_streaming: bool = False,
|
supports_streaming: bool = False,
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
caption_markdown: Optional[str] = None,
|
||||||
|
caption_html: Optional[str] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
return await send_file(
|
return await send_file(
|
||||||
self,
|
self,
|
||||||
|
@ -115,6 +127,9 @@ async def send_video(
|
||||||
height=height,
|
height=height,
|
||||||
round=round,
|
round=round,
|
||||||
supports_streaming=supports_streaming,
|
supports_streaming=supports_streaming,
|
||||||
|
caption=caption,
|
||||||
|
caption_markdown=caption_markdown,
|
||||||
|
caption_html=caption_html,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -319,6 +319,7 @@ class SearchList(MessageList):
|
||||||
self._peer: Optional[abcs.InputPeer] = None
|
self._peer: Optional[abcs.InputPeer] = None
|
||||||
self._limit = limit
|
self._limit = limit
|
||||||
self._query = query
|
self._query = query
|
||||||
|
self._filter = types.InputMessagesFilterEmpty()
|
||||||
self._offset_id = offset_id
|
self._offset_id = offset_id
|
||||||
self._offset_date = offset_date
|
self._offset_date = offset_date
|
||||||
|
|
||||||
|
@ -334,7 +335,7 @@ class SearchList(MessageList):
|
||||||
q=self._query,
|
q=self._query,
|
||||||
from_id=None,
|
from_id=None,
|
||||||
top_msg_id=None,
|
top_msg_id=None,
|
||||||
filter=types.InputMessagesFilterEmpty(),
|
filter=self._filter,
|
||||||
min_date=0,
|
min_date=0,
|
||||||
max_date=self._offset_date,
|
max_date=self._offset_date,
|
||||||
offset_id=self._offset_id,
|
offset_id=self._offset_id,
|
||||||
|
|
|
@ -7,12 +7,10 @@ from typing import (
|
||||||
Any,
|
Any,
|
||||||
Awaitable,
|
Awaitable,
|
||||||
Callable,
|
Callable,
|
||||||
Dict,
|
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from ...session import Gap
|
from ...session import Gap
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from .async_list import AsyncList
|
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 .file import File, InFileLike, InWrapper, OutFileLike, OutWrapper
|
from .file import File, InFileLike, InWrapper, 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 .recent_action import RecentAction
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AsyncList",
|
"AsyncList",
|
||||||
|
@ -17,6 +19,7 @@ __all__ = [
|
||||||
"RestrictionReason",
|
"RestrictionReason",
|
||||||
"User",
|
"User",
|
||||||
"Dialog",
|
"Dialog",
|
||||||
|
"Draft",
|
||||||
"File",
|
"File",
|
||||||
"InFileLike",
|
"InFileLike",
|
||||||
"InWrapper",
|
"InWrapper",
|
||||||
|
@ -27,4 +30,5 @@ __all__ = [
|
||||||
"NoPublicConstructor",
|
"NoPublicConstructor",
|
||||||
"Participant",
|
"Participant",
|
||||||
"PasswordToken",
|
"PasswordToken",
|
||||||
|
"RecentAction",
|
||||||
]
|
]
|
||||||
|
|
29
client/src/telethon/_impl/client/types/draft.py
Normal file
29
client/src/telethon/_impl/client/types/draft.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from typing import Dict, List, Optional, Self
|
||||||
|
|
||||||
|
from ...session import PackedChat, PackedType
|
||||||
|
from ...tl import abcs, types
|
||||||
|
from .chat import Chat
|
||||||
|
from .meta import NoPublicConstructor
|
||||||
|
|
||||||
|
|
||||||
|
class Draft(metaclass=NoPublicConstructor):
|
||||||
|
"""
|
||||||
|
A draft message in a chat.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_raw", "_chat_map")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, raw: types.UpdateDraftMessage, chat_map: Dict[int, Chat]
|
||||||
|
) -> None:
|
||||||
|
self._raw = raw
|
||||||
|
self._chat_map = chat_map
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_raw(
|
||||||
|
cls, draft: types.UpdateDraftMessage, chat_map: Dict[int, Chat]
|
||||||
|
) -> Self:
|
||||||
|
return cls._create(draft, chat_map)
|
||||||
|
|
||||||
|
async def delete(self) -> None:
|
||||||
|
pass
|
|
@ -137,7 +137,7 @@ class File(metaclass=NoPublicConstructor):
|
||||||
self._raw = raw
|
self._raw = raw
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _try_from_raw(cls, raw: abcs.MessageMedia) -> Optional[Self]:
|
def _try_from_raw_message_media(cls, raw: abcs.MessageMedia) -> Optional[Self]:
|
||||||
if isinstance(raw, types.MessageMediaDocument):
|
if isinstance(raw, types.MessageMediaDocument):
|
||||||
if isinstance(raw.document, types.Document):
|
if isinstance(raw.document, types.Document):
|
||||||
return cls._create(
|
return cls._create(
|
||||||
|
@ -176,26 +176,43 @@ class File(metaclass=NoPublicConstructor):
|
||||||
raw=raw,
|
raw=raw,
|
||||||
)
|
)
|
||||||
elif isinstance(raw, types.MessageMediaPhoto):
|
elif isinstance(raw, types.MessageMediaPhoto):
|
||||||
if isinstance(raw.photo, types.Photo):
|
if raw.photo:
|
||||||
|
return cls._try_from_raw_photo(
|
||||||
|
raw.photo, spoiler=raw.spoiler, ttl_seconds=raw.ttl_seconds
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _try_from_raw_photo(
|
||||||
|
cls,
|
||||||
|
raw: abcs.Photo,
|
||||||
|
*,
|
||||||
|
spoiler: bool = False,
|
||||||
|
ttl_seconds: Optional[int] = None,
|
||||||
|
) -> Optional[Self]:
|
||||||
|
if isinstance(raw, types.Photo):
|
||||||
return cls._create(
|
return cls._create(
|
||||||
path=None,
|
path=None,
|
||||||
file=None,
|
file=None,
|
||||||
attributes=[],
|
attributes=[],
|
||||||
size=max(map(photo_size_byte_count, raw.photo.sizes)),
|
size=max(map(photo_size_byte_count, raw.sizes)),
|
||||||
name="",
|
name="",
|
||||||
mime="image/jpeg",
|
mime="image/jpeg",
|
||||||
photo=True,
|
photo=True,
|
||||||
muted=False,
|
muted=False,
|
||||||
input_media=types.InputMediaPhoto(
|
input_media=types.InputMediaPhoto(
|
||||||
spoiler=raw.spoiler,
|
spoiler=spoiler,
|
||||||
id=types.InputPhoto(
|
id=types.InputPhoto(
|
||||||
id=raw.photo.id,
|
id=raw.id,
|
||||||
access_hash=raw.photo.access_hash,
|
access_hash=raw.access_hash,
|
||||||
file_reference=raw.photo.file_reference,
|
file_reference=raw.file_reference,
|
||||||
),
|
),
|
||||||
ttl_seconds=raw.ttl_seconds,
|
ttl_seconds=ttl_seconds,
|
||||||
|
),
|
||||||
|
raw=types.MessageMediaPhoto(
|
||||||
|
spoiler=spoiler, photo=raw, ttl_seconds=ttl_seconds
|
||||||
),
|
),
|
||||||
raw=raw,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -39,8 +39,26 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self) -> int:
|
def id(self) -> int:
|
||||||
|
"""
|
||||||
|
The message identifier.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:doc:`/concepts/messages`, which contains an in-depth explanation of message counters.
|
||||||
|
"""
|
||||||
return self._raw.id
|
return self._raw.id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def grouped_id(self) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
If the message is grouped with others in an album, return the group identifier.
|
||||||
|
|
||||||
|
Messages with the same :attr:`grouped_id` will belong to the same album.
|
||||||
|
|
||||||
|
Note that there can be messages in-between that do not have a :attr:`grouped_id`.
|
||||||
|
"""
|
||||||
|
return getattr(self._raw, "grouped_id", None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self) -> Optional[str]:
|
def text(self) -> Optional[str]:
|
||||||
return getattr(self._raw, "message", None)
|
return getattr(self._raw, "message", None)
|
||||||
|
@ -91,7 +109,7 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
def _file(self) -> Optional[File]:
|
def _file(self) -> Optional[File]:
|
||||||
return (
|
return (
|
||||||
File._try_from_raw(self._raw.media)
|
File._try_from_raw_message_media(self._raw.media)
|
||||||
if isinstance(self._raw, types.Message) and self._raw.media
|
if isinstance(self._raw, types.Message) and self._raw.media
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
29
client/src/telethon/_impl/client/types/recent_action.py
Normal file
29
client/src/telethon/_impl/client/types/recent_action.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from typing import Dict, List, Optional, Self, Union
|
||||||
|
|
||||||
|
from ...session import PackedChat, PackedType
|
||||||
|
from ...tl import abcs, types
|
||||||
|
from .chat import Chat
|
||||||
|
from .meta import NoPublicConstructor
|
||||||
|
|
||||||
|
|
||||||
|
class RecentAction(metaclass=NoPublicConstructor):
|
||||||
|
"""
|
||||||
|
A recent action in a chat, also known as an "admin log event action" or :tl:`ChannelAdminLogEvent`.
|
||||||
|
|
||||||
|
Only administrators of the chat can access these.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_raw", "_chat_map")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
event: abcs.ChannelAdminLogEvent,
|
||||||
|
chat_map: Dict[int, Chat],
|
||||||
|
) -> None:
|
||||||
|
assert isinstance(event, types.ChannelAdminLogEvent)
|
||||||
|
self._raw = event
|
||||||
|
self._chat_map = chat_map
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self) -> int:
|
||||||
|
return self._raw.id
|
|
@ -1,11 +1,11 @@
|
||||||
import struct
|
import struct
|
||||||
from enum import Enum
|
from enum import IntFlag
|
||||||
from typing import Optional, Self
|
from typing import Optional, Self
|
||||||
|
|
||||||
from ...tl import abcs, types
|
from ...tl import abcs, types
|
||||||
|
|
||||||
|
|
||||||
class PackedType(Enum):
|
class PackedType(IntFlag):
|
||||||
"""
|
"""
|
||||||
The type of a :class:`PackedChat`.
|
The type of a :class:`PackedChat`.
|
||||||
"""
|
"""
|
||||||
|
@ -52,6 +52,27 @@ class PackedChat:
|
||||||
ty = PackedType(ty_byte & 0b0011_1111)
|
ty = PackedType(ty_byte & 0b0011_1111)
|
||||||
return cls(ty, id, access_hash if has_hash else None)
|
return cls(ty, id, access_hash if has_hash else None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hex(self) -> str:
|
||||||
|
"""
|
||||||
|
Convenience property to convert to bytes and represent them as hexadecimal numbers:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
assert packed.hex == bytes(packed).hex()
|
||||||
|
"""
|
||||||
|
return bytes(self).hex()
|
||||||
|
|
||||||
|
def from_hex(cls, hex: str) -> Self:
|
||||||
|
"""
|
||||||
|
Convenience method to convert hexadecimal numbers into bytes then passed to :meth:`from_bytes`:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
assert PackedChat.from_hex(packed.hex) == packed
|
||||||
|
"""
|
||||||
|
return cls.from_bytes(bytes.fromhex(hex))
|
||||||
|
|
||||||
def is_user(self) -> bool:
|
def is_user(self) -> bool:
|
||||||
return self.ty in (PackedType.USER, PackedType.BOT)
|
return self.ty in (PackedType.USER, PackedType.BOT)
|
||||||
|
|
||||||
|
@ -93,13 +114,13 @@ class PackedChat:
|
||||||
if self.is_user():
|
if self.is_user():
|
||||||
return types.InputUser(user_id=self.id, access_hash=self.access_hash or 0)
|
return types.InputUser(user_id=self.id, access_hash=self.access_hash or 0)
|
||||||
else:
|
else:
|
||||||
raise ValueError("chat is not user")
|
raise TypeError("chat is not a user")
|
||||||
|
|
||||||
def _to_chat_id(self) -> int:
|
def _to_chat_id(self) -> int:
|
||||||
if self.is_chat():
|
if self.is_chat():
|
||||||
return self.id
|
return self.id
|
||||||
else:
|
else:
|
||||||
raise ValueError("chat is not small group")
|
raise TypeError("chat is not a group")
|
||||||
|
|
||||||
def _to_input_channel(self) -> types.InputChannel:
|
def _to_input_channel(self) -> types.InputChannel:
|
||||||
if self.is_channel():
|
if self.is_channel():
|
||||||
|
@ -107,7 +128,7 @@ class PackedChat:
|
||||||
channel_id=self.id, access_hash=self.access_hash or 0
|
channel_id=self.id, access_hash=self.access_hash or 0
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError("chat is not channel")
|
raise TypeError("chat is not a channel")
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
def __eq__(self, other: object) -> bool:
|
||||||
if not isinstance(other, self.__class__):
|
if not isinstance(other, self.__class__):
|
||||||
|
|
|
@ -11,6 +11,7 @@ Properties and private methods can use a different parameter name than `self`
|
||||||
to avoid being included.
|
to avoid being included.
|
||||||
"""
|
"""
|
||||||
import ast
|
import ast
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Union
|
from typing import Dict, List, Union
|
||||||
|
@ -62,6 +63,7 @@ class MethodVisitor(ast.NodeVisitor):
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
client_root = Path.cwd() / "client/src/telethon/_impl/client/client"
|
client_root = Path.cwd() / "client/src/telethon/_impl/client/client"
|
||||||
|
client_py = client_root / "client.py"
|
||||||
|
|
||||||
fm_visitor = FunctionMethodsVisitor()
|
fm_visitor = FunctionMethodsVisitor()
|
||||||
m_visitor = MethodVisitor()
|
m_visitor = MethodVisitor()
|
||||||
|
@ -74,7 +76,7 @@ def main() -> None:
|
||||||
|
|
||||||
fm_visitor.visit(ast.parse(contents))
|
fm_visitor.visit(ast.parse(contents))
|
||||||
|
|
||||||
with (client_root / "client.py").open(encoding="utf-8") as fd:
|
with client_py.open(encoding="utf-8") as fd:
|
||||||
contents = fd.read()
|
contents = fd.read()
|
||||||
|
|
||||||
m_visitor.visit(ast.parse(contents))
|
m_visitor.visit(ast.parse(contents))
|
||||||
|
@ -109,13 +111,22 @@ def main() -> None:
|
||||||
function.body.append(call)
|
function.body.append(call)
|
||||||
class_body.append(function)
|
class_body.append(function)
|
||||||
|
|
||||||
print(
|
generated = ast.unparse(
|
||||||
ast.unparse(
|
|
||||||
ast.ClassDef(
|
ast.ClassDef(
|
||||||
name="Client", bases=[], keywords=[], body=class_body, decorator_list=[]
|
name="Client", bases=[], keywords=[], body=class_body, decorator_list=[]
|
||||||
)
|
)
|
||||||
|
)[len("class Client:") :].strip()
|
||||||
|
|
||||||
|
start_idx = contents.index("\n", contents.index("# Begin partially @generated"))
|
||||||
|
end_idx = contents.index("# End partially @generated")
|
||||||
|
|
||||||
|
with client_py.open("w", encoding="utf-8") as fd:
|
||||||
|
fd.write(
|
||||||
|
f"{contents[:start_idx]}\n\n {generated}\n\n {contents[end_idx:]}"
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
print("written @generated")
|
||||||
|
exit(subprocess.run((sys.executable, "-m", "black", str(client_py))).returncode)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
Loading…
Reference in New Issue
Block a user