mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-01-24 16:24:15 +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`.
|
||||
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.
|
||||
For this tutorial, we'll be using the ``asyncio`` 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 :mod:`asyncio` REPL:
|
||||
|
||||
.. 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.
|
||||
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 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.
|
||||
|
||||
HTTP Bot API
|
||||
Telegram's simplified HTTP API to control bot accounts only.
|
||||
|
||||
.. seealso:: The :doc:`../concepts/botapi-vs-mtproto` concept.
|
||||
|
||||
login
|
||||
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.
|
||||
|
|
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_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.
|
||||
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
|
||||
|
@ -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.
|
||||
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:
|
||||
``.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.
|
||||
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.
|
||||
You can use these for your type-hinting needs.
|
||||
* ``tl.types`` contains concrete instances, the "bare" types Telegram actually returns.
|
||||
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
|
||||
|
@ -159,6 +201,66 @@ The simplest approach could be using a global ``states`` dictionary storing the
|
|||
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
|
||||
--------------------------------------
|
||||
|
@ -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`.
|
||||
|
||||
|
||||
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
|
||||
----------------------------------------
|
||||
|
||||
|
@ -232,6 +385,13 @@ Overall, dealing with users, groups and channels should feel a lot more natural.
|
|||
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
|
||||
------------------------------
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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
|
||||
|
@ -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.
|
||||
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
|
||||
---------------------
|
||||
|
|
|
@ -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).
|
||||
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.
|
||||
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/updates
|
||||
concepts/messages
|
||||
concepts/sessions
|
||||
concepts/errors
|
||||
concepts/botapi-vs-mtproto
|
||||
concepts/full-api
|
||||
concepts/glossary
|
||||
|
||||
Developing
|
||||
==========
|
||||
Development resources
|
||||
=====================
|
||||
|
||||
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::
|
||||
:hidden:
|
||||
:caption: Developing
|
||||
:caption: Development resources
|
||||
|
||||
developing/changelog
|
||||
developing/migration-guide
|
||||
developing/faq
|
||||
developing/philosophy.rst
|
||||
developing/coding-style.rst
|
||||
developing/project-structure.rst
|
||||
|
|
|
@ -31,6 +31,7 @@ Errors
|
|||
except errors.FloodWait as e:
|
||||
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:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
|
@ -3,8 +3,9 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
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 .messages import SearchList
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import Client
|
||||
|
@ -81,3 +82,106 @@ class ParticipantList(AsyncList[Participant]):
|
|||
|
||||
def get_participants(self: Client, chat: ChatLike) -> AsyncList[Participant]:
|
||||
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,
|
||||
ChatLike,
|
||||
Dialog,
|
||||
Draft,
|
||||
File,
|
||||
InFileLike,
|
||||
LoginToken,
|
||||
|
@ -43,6 +44,7 @@ from ..types import (
|
|||
OutFileLike,
|
||||
Participant,
|
||||
PasswordToken,
|
||||
RecentAction,
|
||||
User,
|
||||
)
|
||||
from .auth import (
|
||||
|
@ -55,8 +57,15 @@ from .auth import (
|
|||
sign_out,
|
||||
)
|
||||
from .bots import InlineResult, inline_query
|
||||
from .chats import get_participants
|
||||
from .dialogs import delete_dialog, get_dialogs
|
||||
from .chats import (
|
||||
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 (
|
||||
download,
|
||||
get_file_bytes,
|
||||
|
@ -198,7 +207,7 @@ class Client:
|
|||
if self._config.catch_up and self._config.session.state:
|
||||
self._message_box.load(self._config.session.state)
|
||||
|
||||
# ---
|
||||
# Begin partially @generated
|
||||
|
||||
def add_event_handler(
|
||||
self,
|
||||
|
@ -511,6 +520,29 @@ class Client:
|
|||
"""
|
||||
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]:
|
||||
"""
|
||||
Get the users in your contact list.
|
||||
|
@ -548,6 +580,47 @@ class Client:
|
|||
"""
|
||||
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(
|
||||
self, handler: Callable[[Event], Awaitable[Any]]
|
||||
) -> Optional[Filter]:
|
||||
|
@ -666,6 +739,23 @@ class Client:
|
|||
"""
|
||||
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(
|
||||
self, bot: ChatLike, query: str = "", *, chat: Optional[ChatLike] = None
|
||||
) -> AsyncIterator[InlineResult]:
|
||||
|
@ -730,34 +820,15 @@ class Client:
|
|||
Check whether the client instance is authorized (i.e. logged-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
|
||||
|
||||
.. 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')
|
||||
if not await client.is_authorized():
|
||||
... # need to sign in
|
||||
"""
|
||||
return get_file_bytes(self, media)
|
||||
return await is_authorized(self)
|
||||
|
||||
def on(
|
||||
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.
|
||||
|
||||
: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(
|
||||
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.
|
||||
|
||||
: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(
|
||||
self, chat, limit, query=query, offset_id=offset_id, offset_date=offset_date
|
||||
|
@ -988,6 +1073,9 @@ class Client:
|
|||
voice: bool = False,
|
||||
title: Optional[str] = None,
|
||||
performer: Optional[str] = None,
|
||||
caption: Optional[str] = None,
|
||||
caption_markdown: Optional[str] = None,
|
||||
caption_html: Optional[str] = None,
|
||||
) -> Message:
|
||||
"""
|
||||
Send an audio file.
|
||||
|
@ -1002,6 +1090,12 @@ class Client:
|
|||
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`.
|
||||
|
||||
.. rubric:: Example
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
await client.send_audio(chat, 'file.ogg', voice=True)
|
||||
"""
|
||||
return await send_audio(
|
||||
self,
|
||||
|
@ -1015,6 +1109,9 @@ class Client:
|
|||
voice=voice,
|
||||
title=title,
|
||||
performer=performer,
|
||||
caption=caption,
|
||||
caption_markdown=caption_markdown,
|
||||
caption_html=caption_html,
|
||||
)
|
||||
|
||||
async def send_file(
|
||||
|
@ -1072,7 +1169,16 @@ class Client:
|
|||
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 section on :doc:`/concepts/messages` to learn about message formatting.
|
||||
|
||||
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(
|
||||
self,
|
||||
|
@ -1120,20 +1226,23 @@ class Client:
|
|||
Message text, with no formatting.
|
||||
|
||||
:param text_markdown:
|
||||
Message text, parsed as markdown.
|
||||
Message text, parsed as CommonMark.
|
||||
|
||||
:param text_html:
|
||||
Message text, parsed as HTML.
|
||||
|
||||
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(
|
||||
self,
|
||||
chat,
|
||||
text,
|
||||
markdown=markdown,
|
||||
html=html,
|
||||
link_preview=link_preview,
|
||||
self, chat, text, markdown=markdown, html=html, link_preview=link_preview
|
||||
)
|
||||
|
||||
async def send_photo(
|
||||
|
@ -1148,6 +1257,9 @@ class Client:
|
|||
compress: bool = True,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
caption: Optional[str] = None,
|
||||
caption_markdown: Optional[str] = None,
|
||||
caption_html: Optional[str] = None,
|
||||
) -> Message:
|
||||
"""
|
||||
Send a photo file.
|
||||
|
@ -1166,6 +1278,12 @@ class Client:
|
|||
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`.
|
||||
|
||||
.. rubric:: Example
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
await client.send_photo(chat, 'photo.jpg', caption='Check this out!')
|
||||
"""
|
||||
return await send_photo(
|
||||
self,
|
||||
|
@ -1178,6 +1296,9 @@ class Client:
|
|||
compress=compress,
|
||||
width=width,
|
||||
height=height,
|
||||
caption=caption,
|
||||
caption_markdown=caption_markdown,
|
||||
caption_html=caption_html,
|
||||
)
|
||||
|
||||
async def send_video(
|
||||
|
@ -1194,6 +1315,9 @@ class Client:
|
|||
height: Optional[int] = None,
|
||||
round: bool = False,
|
||||
supports_streaming: bool = False,
|
||||
caption: Optional[str] = None,
|
||||
caption_markdown: Optional[str] = None,
|
||||
caption_html: Optional[str] = None,
|
||||
) -> Message:
|
||||
"""
|
||||
Send a video file.
|
||||
|
@ -1208,6 +1332,12 @@ class Client:
|
|||
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`.
|
||||
|
||||
.. rubric:: Example
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
await client.send_video(chat, 'video.mp4', caption_markdown='*I cannot believe this just happened*')
|
||||
"""
|
||||
return await send_video(
|
||||
self,
|
||||
|
@ -1222,8 +1352,20 @@ class Client:
|
|||
height=height,
|
||||
round=round,
|
||||
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(
|
||||
self,
|
||||
handler: Callable[[Event], Awaitable[Any]],
|
||||
|
@ -1314,7 +1456,7 @@ class Client:
|
|||
"""
|
||||
await unpin_message(self, chat, message_id)
|
||||
|
||||
# ---
|
||||
# End partially @generated
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
|
|
|
@ -2,8 +2,8 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...tl import abcs, functions, types
|
||||
from ..types import AsyncList, ChatLike, Dialog, User
|
||||
from ...tl import functions, types
|
||||
from ..types import AsyncList, ChatLike, Dialog, Draft
|
||||
from ..utils import build_chat_map
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -78,3 +78,29 @@ async def delete_dialog(self: Client, chat: ChatLike) -> 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
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
from functools import partial
|
||||
from inspect import isawaitable
|
||||
from io import BufferedWriter
|
||||
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 ..types import (
|
||||
|
@ -43,6 +40,9 @@ async def send_photo(
|
|||
compress: bool = True,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
caption: Optional[str] = None,
|
||||
caption_markdown: Optional[str] = None,
|
||||
caption_html: Optional[str] = None,
|
||||
) -> Message:
|
||||
return await send_file(
|
||||
self,
|
||||
|
@ -55,6 +55,9 @@ async def send_photo(
|
|||
compress=compress,
|
||||
width=width,
|
||||
height=height,
|
||||
caption=caption,
|
||||
caption_markdown=caption_markdown,
|
||||
caption_html=caption_html,
|
||||
)
|
||||
|
||||
|
||||
|
@ -71,6 +74,9 @@ async def send_audio(
|
|||
voice: bool = False,
|
||||
title: Optional[str] = None,
|
||||
performer: Optional[str] = None,
|
||||
caption: Optional[str] = None,
|
||||
caption_markdown: Optional[str] = None,
|
||||
caption_html: Optional[str] = None,
|
||||
) -> Message:
|
||||
return await send_file(
|
||||
self,
|
||||
|
@ -84,6 +90,9 @@ async def send_audio(
|
|||
voice=voice,
|
||||
title=title,
|
||||
performer=performer,
|
||||
caption=caption,
|
||||
caption_markdown=caption_markdown,
|
||||
caption_html=caption_html,
|
||||
)
|
||||
|
||||
|
||||
|
@ -101,6 +110,9 @@ async def send_video(
|
|||
height: Optional[int] = None,
|
||||
round: bool = False,
|
||||
supports_streaming: bool = False,
|
||||
caption: Optional[str] = None,
|
||||
caption_markdown: Optional[str] = None,
|
||||
caption_html: Optional[str] = None,
|
||||
) -> Message:
|
||||
return await send_file(
|
||||
self,
|
||||
|
@ -115,6 +127,9 @@ async def send_video(
|
|||
height=height,
|
||||
round=round,
|
||||
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._limit = limit
|
||||
self._query = query
|
||||
self._filter = types.InputMessagesFilterEmpty()
|
||||
self._offset_id = offset_id
|
||||
self._offset_date = offset_date
|
||||
|
||||
|
@ -334,7 +335,7 @@ class SearchList(MessageList):
|
|||
q=self._query,
|
||||
from_id=None,
|
||||
top_msg_id=None,
|
||||
filter=types.InputMessagesFilterEmpty(),
|
||||
filter=self._filter,
|
||||
min_date=0,
|
||||
max_date=self._offset_date,
|
||||
offset_id=self._offset_id,
|
||||
|
|
|
@ -7,12 +7,10 @@ from typing import (
|
|||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from ...session import Gap
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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 .login_token import LoginToken
|
||||
from .message import Message
|
||||
from .meta import NoPublicConstructor
|
||||
from .participant import Participant
|
||||
from .password_token import PasswordToken
|
||||
from .recent_action import RecentAction
|
||||
|
||||
__all__ = [
|
||||
"AsyncList",
|
||||
|
@ -17,6 +19,7 @@ __all__ = [
|
|||
"RestrictionReason",
|
||||
"User",
|
||||
"Dialog",
|
||||
"Draft",
|
||||
"File",
|
||||
"InFileLike",
|
||||
"InWrapper",
|
||||
|
@ -27,4 +30,5 @@ __all__ = [
|
|||
"NoPublicConstructor",
|
||||
"Participant",
|
||||
"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
|
||||
|
||||
@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.document, types.Document):
|
||||
return cls._create(
|
||||
|
@ -176,26 +176,43 @@ class File(metaclass=NoPublicConstructor):
|
|||
raw=raw,
|
||||
)
|
||||
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(
|
||||
path=None,
|
||||
file=None,
|
||||
attributes=[],
|
||||
size=max(map(photo_size_byte_count, raw.photo.sizes)),
|
||||
size=max(map(photo_size_byte_count, raw.sizes)),
|
||||
name="",
|
||||
mime="image/jpeg",
|
||||
photo=True,
|
||||
muted=False,
|
||||
input_media=types.InputMediaPhoto(
|
||||
spoiler=raw.spoiler,
|
||||
spoiler=spoiler,
|
||||
id=types.InputPhoto(
|
||||
id=raw.photo.id,
|
||||
access_hash=raw.photo.access_hash,
|
||||
file_reference=raw.photo.file_reference,
|
||||
id=raw.id,
|
||||
access_hash=raw.access_hash,
|
||||
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
|
||||
|
|
|
@ -39,8 +39,26 @@ class Message(metaclass=NoPublicConstructor):
|
|||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
"""
|
||||
The message identifier.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:doc:`/concepts/messages`, which contains an in-depth explanation of message counters.
|
||||
"""
|
||||
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
|
||||
def text(self) -> Optional[str]:
|
||||
return getattr(self._raw, "message", None)
|
||||
|
@ -91,7 +109,7 @@ class Message(metaclass=NoPublicConstructor):
|
|||
|
||||
def _file(self) -> Optional[File]:
|
||||
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
|
||||
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
|
||||
from enum import Enum
|
||||
from enum import IntFlag
|
||||
from typing import Optional, Self
|
||||
|
||||
from ...tl import abcs, types
|
||||
|
||||
|
||||
class PackedType(Enum):
|
||||
class PackedType(IntFlag):
|
||||
"""
|
||||
The type of a :class:`PackedChat`.
|
||||
"""
|
||||
|
@ -52,6 +52,27 @@ class PackedChat:
|
|||
ty = PackedType(ty_byte & 0b0011_1111)
|
||||
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:
|
||||
return self.ty in (PackedType.USER, PackedType.BOT)
|
||||
|
||||
|
@ -93,13 +114,13 @@ class PackedChat:
|
|||
if self.is_user():
|
||||
return types.InputUser(user_id=self.id, access_hash=self.access_hash or 0)
|
||||
else:
|
||||
raise ValueError("chat is not user")
|
||||
raise TypeError("chat is not a user")
|
||||
|
||||
def _to_chat_id(self) -> int:
|
||||
if self.is_chat():
|
||||
return self.id
|
||||
else:
|
||||
raise ValueError("chat is not small group")
|
||||
raise TypeError("chat is not a group")
|
||||
|
||||
def _to_input_channel(self) -> types.InputChannel:
|
||||
if self.is_channel():
|
||||
|
@ -107,7 +128,7 @@ class PackedChat:
|
|||
channel_id=self.id, access_hash=self.access_hash or 0
|
||||
)
|
||||
else:
|
||||
raise ValueError("chat is not channel")
|
||||
raise TypeError("chat is not a channel")
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
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.
|
||||
"""
|
||||
import ast
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Union
|
||||
|
@ -62,6 +63,7 @@ class MethodVisitor(ast.NodeVisitor):
|
|||
|
||||
def main() -> None:
|
||||
client_root = Path.cwd() / "client/src/telethon/_impl/client/client"
|
||||
client_py = client_root / "client.py"
|
||||
|
||||
fm_visitor = FunctionMethodsVisitor()
|
||||
m_visitor = MethodVisitor()
|
||||
|
@ -74,7 +76,7 @@ def main() -> None:
|
|||
|
||||
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()
|
||||
|
||||
m_visitor.visit(ast.parse(contents))
|
||||
|
@ -109,13 +111,22 @@ def main() -> None:
|
|||
function.body.append(call)
|
||||
class_body.append(function)
|
||||
|
||||
print(
|
||||
ast.unparse(
|
||||
generated = ast.unparse(
|
||||
ast.ClassDef(
|
||||
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__":
|
||||
|
|
Loading…
Reference in New Issue
Block a user