mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-07-15 10:32:28 +03:00
Merge branch 'LonamiWebs:v2' into v2
This commit is contained in:
commit
96dd15f796
|
@ -5,6 +5,8 @@ build:
|
||||||
os: ubuntu-22.04
|
os: ubuntu-22.04
|
||||||
tools:
|
tools:
|
||||||
python: "3.11"
|
python: "3.11"
|
||||||
|
apt_packages:
|
||||||
|
- graphviz
|
||||||
|
|
||||||
sphinx:
|
sphinx:
|
||||||
configuration: client/doc/conf.py
|
configuration: client/doc/conf.py
|
||||||
|
|
|
@ -318,7 +318,7 @@ In Telethon:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from telethon import Client, events
|
from telethon import Client, events
|
||||||
from telethon.events.filters import Any, Command, TextOnly
|
from telethon.events.filters import Any, Command, Media
|
||||||
bot = Client('bot', api_id, api_hash)
|
bot = Client('bot', api_id, api_hash)
|
||||||
|
|
||||||
# Handle '/start' and '/help'
|
# Handle '/start' and '/help'
|
||||||
|
@ -329,8 +329,8 @@ In Telethon:
|
||||||
I am here to echo your kind words back to you. Just say anything nice and I'll say the exact same thing to you!\
|
I am here to echo your kind words back to you. Just say anything nice and I'll say the exact same thing to you!\
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Handle all other messages with only 'text'
|
# Handle all other messages without media (negating the filter using ~)
|
||||||
@bot.on(events.NewMessage, TextOnly())
|
@bot.on(events.NewMessage, ~Media())
|
||||||
async def echo_message(message: NewMessage):
|
async def echo_message(message: NewMessage):
|
||||||
await message.reply(message.text)
|
await message.reply(message.text)
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,7 @@ Note that `CommonMark's markdown <https://commonmark.org/>`_ is not fully compat
|
||||||
```
|
```
|
||||||
|
|
||||||
HTML is also not fully compatible with :term:`HTTP Bot API`'s
|
HTML is also not fully compatible with :term:`HTTP Bot API`'s
|
||||||
`MarkdownV2 style <https://core.telegram.org/bots/api#markdownv2-style>`_,
|
`HTML style <https://core.telegram.org/bots/api#html-style>`_,
|
||||||
and instead favours more standard `HTML elements <https://developer.mozilla.org/en-US/docs/Web/HTML/Element>`_:
|
and instead favours more standard `HTML elements <https://developer.mozilla.org/en-US/docs/Web/HTML/Element>`_:
|
||||||
|
|
||||||
* ``strong`` and ``b`` for **bold**.
|
* ``strong`` and ``b`` for **bold**.
|
||||||
|
|
|
@ -37,6 +37,7 @@ from ..events import Event
|
||||||
from ..events.filters import Filter
|
from ..events.filters import Filter
|
||||||
from ..types import (
|
from ..types import (
|
||||||
AdminRight,
|
AdminRight,
|
||||||
|
AlbumBuilder,
|
||||||
AsyncList,
|
AsyncList,
|
||||||
Chat,
|
Chat,
|
||||||
ChatLike,
|
ChatLike,
|
||||||
|
@ -53,8 +54,8 @@ from ..types import (
|
||||||
PasswordToken,
|
PasswordToken,
|
||||||
RecentAction,
|
RecentAction,
|
||||||
User,
|
User,
|
||||||
buttons,
|
|
||||||
)
|
)
|
||||||
|
from ..types import buttons as btns
|
||||||
from .auth import (
|
from .auth import (
|
||||||
bot_sign_in,
|
bot_sign_in,
|
||||||
check_password,
|
check_password,
|
||||||
|
@ -77,10 +78,12 @@ from .dialogs import delete_dialog, edit_draft, get_dialogs, get_drafts
|
||||||
from .files import (
|
from .files import (
|
||||||
download,
|
download,
|
||||||
get_file_bytes,
|
get_file_bytes,
|
||||||
|
prepare_album,
|
||||||
send_audio,
|
send_audio,
|
||||||
send_file,
|
send_file,
|
||||||
send_photo,
|
send_photo,
|
||||||
send_video,
|
send_video,
|
||||||
|
upload,
|
||||||
)
|
)
|
||||||
from .messages import (
|
from .messages import (
|
||||||
MessageMap,
|
MessageMap,
|
||||||
|
@ -115,6 +118,7 @@ from .updates import (
|
||||||
set_handler_filter,
|
set_handler_filter,
|
||||||
)
|
)
|
||||||
from .users import (
|
from .users import (
|
||||||
|
get_chats,
|
||||||
get_contacts,
|
get_contacts,
|
||||||
get_me,
|
get_me,
|
||||||
input_to_peer,
|
input_to_peer,
|
||||||
|
@ -680,6 +684,40 @@ class Client:
|
||||||
"""
|
"""
|
||||||
return get_admin_log(self, chat)
|
return get_admin_log(self, chat)
|
||||||
|
|
||||||
|
async def get_chats(self, chats: Sequence[ChatLike]) -> List[Chat]:
|
||||||
|
"""
|
||||||
|
Get the latest basic information about the given chats.
|
||||||
|
|
||||||
|
This method is most commonly used to turn one or more :class:`~types.PackedChat` into the original :class:`~types.Chat`.
|
||||||
|
This includes users, groups and broadcast channels.
|
||||||
|
|
||||||
|
:param chats:
|
||||||
|
The users, groups or channels to fetch.
|
||||||
|
|
||||||
|
:return: The fetched chats.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Retrieve a PackedChat from somewhere
|
||||||
|
packed_user = my_database.get_packed_winner()
|
||||||
|
|
||||||
|
# Fetch it
|
||||||
|
users = await client.get_chats([packed_user])
|
||||||
|
user = users[0] # user will be a User if our packed_user was a user
|
||||||
|
|
||||||
|
# Notify the user they won, using their current full name in the message
|
||||||
|
await client.send_message(packed_user, f'Congratulations {user.name}, you won!')
|
||||||
|
|
||||||
|
.. caution::
|
||||||
|
|
||||||
|
This method supports being called with anything that looks like a chat, like every other method.
|
||||||
|
However, calling it with usernames or phone numbers will fetch the chats twice.
|
||||||
|
If that's the case, consider using :meth:`resolve_username` or :meth:`get_contacts` instead.
|
||||||
|
"""
|
||||||
|
return await get_chats(self, chats)
|
||||||
|
|
||||||
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.
|
||||||
|
@ -1085,6 +1123,34 @@ class Client:
|
||||||
"""
|
"""
|
||||||
return await pin_message(self, chat, message_id)
|
return await pin_message(self, chat, message_id)
|
||||||
|
|
||||||
|
def prepare_album(self) -> AlbumBuilder:
|
||||||
|
"""
|
||||||
|
Prepare an album upload to send.
|
||||||
|
|
||||||
|
Albums are a way to send multiple photos or videos as separate messages with the same grouped identifier.
|
||||||
|
|
||||||
|
:return: A new album builder instance, with no media added to it yet.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Prepare a new album
|
||||||
|
album = await client.prepare_album()
|
||||||
|
|
||||||
|
# Add a bunch of photos
|
||||||
|
for photo in ('a.jpg', 'b.png'):
|
||||||
|
await album.add_photo(photo)
|
||||||
|
# A video in-between
|
||||||
|
await album.add_video('c.mp4')
|
||||||
|
# And another photo
|
||||||
|
await album.add_photo('d.jpeg')
|
||||||
|
|
||||||
|
# Album is ready to be sent to as many chats as needed
|
||||||
|
await album.send(chat)
|
||||||
|
"""
|
||||||
|
return prepare_album(self)
|
||||||
|
|
||||||
async def read_message(
|
async def read_message(
|
||||||
self, chat: ChatLike, message_id: Union[int, Literal["all"]]
|
self, chat: ChatLike, message_id: Union[int, Literal["all"]]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -1169,28 +1235,6 @@ class Client:
|
||||||
"""
|
"""
|
||||||
return await request_login_code(self, phone)
|
return await request_login_code(self, phone)
|
||||||
|
|
||||||
async def resolve_to_packed(self, chat: ChatLike) -> PackedChat:
|
|
||||||
"""
|
|
||||||
Resolve a :term:`chat` and return a compact, reusable reference to it.
|
|
||||||
|
|
||||||
:param chat:
|
|
||||||
The :term:`chat` to resolve.
|
|
||||||
|
|
||||||
:return: An efficient, reusable version of the input.
|
|
||||||
|
|
||||||
.. rubric:: Example
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
friend = await client.resolve_to_packed('@cat')
|
|
||||||
# Now you can use `friend` to get or send messages, files...
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
|
|
||||||
In-depth explanation for :doc:`/concepts/chats`.
|
|
||||||
"""
|
|
||||||
return await resolve_to_packed(self, chat)
|
|
||||||
|
|
||||||
async def resolve_username(self, username: str) -> Chat:
|
async def resolve_username(self, username: str) -> Chat:
|
||||||
"""
|
"""
|
||||||
Resolve a username into a :term:`chat`.
|
Resolve a username into a :term:`chat`.
|
||||||
|
@ -1310,6 +1354,7 @@ class Client:
|
||||||
self,
|
self,
|
||||||
chat: ChatLike,
|
chat: ChatLike,
|
||||||
file: Union[str, Path, InFileLike, File],
|
file: Union[str, Path, InFileLike, File],
|
||||||
|
mime_type: Optional[str] = None,
|
||||||
*,
|
*,
|
||||||
size: Optional[int] = None,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
|
@ -1320,6 +1365,8 @@ class Client:
|
||||||
caption: Optional[str] = None,
|
caption: Optional[str] = None,
|
||||||
caption_markdown: Optional[str] = None,
|
caption_markdown: Optional[str] = None,
|
||||||
caption_html: Optional[str] = None,
|
caption_html: Optional[str] = None,
|
||||||
|
reply_to: Optional[int] = None,
|
||||||
|
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Send an audio file.
|
Send an audio file.
|
||||||
|
@ -1333,6 +1380,7 @@ class Client:
|
||||||
:param file: See :meth:`send_file`.
|
:param file: See :meth:`send_file`.
|
||||||
:param size: See :meth:`send_file`.
|
:param size: See :meth:`send_file`.
|
||||||
:param name: See :meth:`send_file`.
|
:param name: See :meth:`send_file`.
|
||||||
|
:param mime_type: See :meth:`send_file`.
|
||||||
:param duration: See :meth:`send_file`.
|
:param duration: See :meth:`send_file`.
|
||||||
:param voice: See :meth:`send_file`.
|
:param voice: See :meth:`send_file`.
|
||||||
:param title: See :meth:`send_file`.
|
:param title: See :meth:`send_file`.
|
||||||
|
@ -1351,6 +1399,7 @@ class Client:
|
||||||
self,
|
self,
|
||||||
chat,
|
chat,
|
||||||
file,
|
file,
|
||||||
|
mime_type,
|
||||||
size=size,
|
size=size,
|
||||||
name=name,
|
name=name,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
|
@ -1360,6 +1409,8 @@ class Client:
|
||||||
caption=caption,
|
caption=caption,
|
||||||
caption_markdown=caption_markdown,
|
caption_markdown=caption_markdown,
|
||||||
caption_html=caption_html,
|
caption_html=caption_html,
|
||||||
|
reply_to=reply_to,
|
||||||
|
buttons=buttons,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def send_file(
|
async def send_file(
|
||||||
|
@ -1386,6 +1437,8 @@ class Client:
|
||||||
caption: Optional[str] = None,
|
caption: Optional[str] = None,
|
||||||
caption_markdown: Optional[str] = None,
|
caption_markdown: Optional[str] = None,
|
||||||
caption_html: Optional[str] = None,
|
caption_html: Optional[str] = None,
|
||||||
|
reply_to: Optional[int] = None,
|
||||||
|
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Send any type of file with any amount of attributes.
|
Send any type of file with any amount of attributes.
|
||||||
|
@ -1438,8 +1491,10 @@ class Client:
|
||||||
|
|
||||||
When given a string or path, its :attr:`~pathlib.PurePath.name` will be used by default only if this parameter is omitted.
|
When given a string or path, its :attr:`~pathlib.PurePath.name` will be used by default only if this parameter is omitted.
|
||||||
|
|
||||||
This parameter **must** be specified when sending a previously-opened or in-memory files.
|
When given a :term:`file-like object`, if it has a ``.name`` :class:`str` property, it will be used.
|
||||||
The library will not attempt to read any ``name`` attributes the object may have.
|
This is the case for files opened via :func:`open`.
|
||||||
|
|
||||||
|
This parameter **must** be specified when sending any other previously-opened or in-memory files.
|
||||||
|
|
||||||
:param mime_type:
|
:param mime_type:
|
||||||
Override for the default mime-type.
|
Override for the default mime-type.
|
||||||
|
@ -1536,6 +1591,8 @@ class Client:
|
||||||
caption=caption,
|
caption=caption,
|
||||||
caption_markdown=caption_markdown,
|
caption_markdown=caption_markdown,
|
||||||
caption_html=caption_html,
|
caption_html=caption_html,
|
||||||
|
reply_to=reply_to,
|
||||||
|
buttons=buttons,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def send_message(
|
async def send_message(
|
||||||
|
@ -1547,9 +1604,7 @@ class Client:
|
||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
link_preview: bool = False,
|
link_preview: bool = False,
|
||||||
reply_to: Optional[int] = None,
|
reply_to: Optional[int] = None,
|
||||||
buttons: Optional[
|
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||||
Union[List[buttons.Button], List[List[buttons.Button]]]
|
|
||||||
] = None,
|
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Send a message.
|
Send a message.
|
||||||
|
@ -1594,12 +1649,15 @@ class Client:
|
||||||
*,
|
*,
|
||||||
size: Optional[int] = None,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
|
mime_type: Optional[str] = None,
|
||||||
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: Optional[str] = None,
|
||||||
caption_markdown: Optional[str] = None,
|
caption_markdown: Optional[str] = None,
|
||||||
caption_html: Optional[str] = None,
|
caption_html: Optional[str] = None,
|
||||||
|
reply_to: Optional[int] = None,
|
||||||
|
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Send a photo file.
|
Send a photo file.
|
||||||
|
@ -1617,6 +1675,7 @@ class Client:
|
||||||
:param file: See :meth:`send_file`.
|
:param file: See :meth:`send_file`.
|
||||||
:param size: See :meth:`send_file`.
|
:param size: See :meth:`send_file`.
|
||||||
:param name: See :meth:`send_file`.
|
:param name: See :meth:`send_file`.
|
||||||
|
:param mime_type: See :meth:`send_file`.
|
||||||
:param compress: See :meth:`send_file`.
|
:param compress: See :meth:`send_file`.
|
||||||
:param width: See :meth:`send_file`.
|
:param width: See :meth:`send_file`.
|
||||||
:param height: See :meth:`send_file`.
|
:param height: See :meth:`send_file`.
|
||||||
|
@ -1636,12 +1695,15 @@ class Client:
|
||||||
file,
|
file,
|
||||||
size=size,
|
size=size,
|
||||||
name=name,
|
name=name,
|
||||||
|
mime_type=mime_type,
|
||||||
compress=compress,
|
compress=compress,
|
||||||
width=width,
|
width=width,
|
||||||
height=height,
|
height=height,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
caption_markdown=caption_markdown,
|
caption_markdown=caption_markdown,
|
||||||
caption_html=caption_html,
|
caption_html=caption_html,
|
||||||
|
reply_to=reply_to,
|
||||||
|
buttons=buttons,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def send_video(
|
async def send_video(
|
||||||
|
@ -1651,14 +1713,18 @@ class Client:
|
||||||
*,
|
*,
|
||||||
size: Optional[int] = None,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
|
mime_type: Optional[str] = None,
|
||||||
duration: Optional[float] = None,
|
duration: Optional[float] = None,
|
||||||
width: Optional[int] = None,
|
width: Optional[int] = None,
|
||||||
height: Optional[int] = None,
|
height: Optional[int] = None,
|
||||||
round: bool = False,
|
round: bool = False,
|
||||||
supports_streaming: bool = False,
|
supports_streaming: bool = False,
|
||||||
|
muted: bool = False,
|
||||||
caption: Optional[str] = None,
|
caption: Optional[str] = None,
|
||||||
caption_markdown: Optional[str] = None,
|
caption_markdown: Optional[str] = None,
|
||||||
caption_html: Optional[str] = None,
|
caption_html: Optional[str] = None,
|
||||||
|
reply_to: Optional[int] = None,
|
||||||
|
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
"""
|
"""
|
||||||
Send a video file.
|
Send a video file.
|
||||||
|
@ -1672,6 +1738,7 @@ class Client:
|
||||||
:param file: See :meth:`send_file`.
|
:param file: See :meth:`send_file`.
|
||||||
:param size: See :meth:`send_file`.
|
:param size: See :meth:`send_file`.
|
||||||
:param name: See :meth:`send_file`.
|
:param name: See :meth:`send_file`.
|
||||||
|
:param mime_type: See :meth:`send_file`.
|
||||||
:param duration: See :meth:`send_file`.
|
:param duration: See :meth:`send_file`.
|
||||||
:param width: See :meth:`send_file`.
|
:param width: See :meth:`send_file`.
|
||||||
:param height: See :meth:`send_file`.
|
:param height: See :meth:`send_file`.
|
||||||
|
@ -1693,14 +1760,18 @@ class Client:
|
||||||
file,
|
file,
|
||||||
size=size,
|
size=size,
|
||||||
name=name,
|
name=name,
|
||||||
|
mime_type=mime_type,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
width=width,
|
width=width,
|
||||||
height=height,
|
height=height,
|
||||||
round=round,
|
round=round,
|
||||||
supports_streaming=supports_streaming,
|
supports_streaming=supports_streaming,
|
||||||
|
muted=muted,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
caption_markdown=caption_markdown,
|
caption_markdown=caption_markdown,
|
||||||
caption_html=caption_html,
|
caption_html=caption_html,
|
||||||
|
reply_to=reply_to,
|
||||||
|
buttons=buttons,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def set_chat_default_restrictions(
|
async def set_chat_default_restrictions(
|
||||||
|
@ -1961,6 +2032,11 @@ class Client:
|
||||||
def _input_to_peer(self, input: Optional[abcs.InputPeer]) -> Optional[abcs.Peer]:
|
def _input_to_peer(self, input: Optional[abcs.InputPeer]) -> Optional[abcs.Peer]:
|
||||||
return input_to_peer(self, input)
|
return input_to_peer(self, input)
|
||||||
|
|
||||||
|
async def _upload(
|
||||||
|
self, fd: Union[str, Path, InFileLike], size: Optional[int], name: Optional[str]
|
||||||
|
) -> Tuple[abcs.InputFile, str]:
|
||||||
|
return await upload(self, fd, size, name)
|
||||||
|
|
||||||
async def __call__(self, request: Request[Return]) -> Return:
|
async def __call__(self, request: Request[Return]) -> Return:
|
||||||
if not self._sender:
|
if not self._sender:
|
||||||
raise ConnectionError("not connected")
|
raise ConnectionError("not connected")
|
||||||
|
|
|
@ -4,8 +4,15 @@ import time
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from ...tl import functions, types
|
from ...tl import functions, types
|
||||||
from ..types import AsyncList, ChatLike, Dialog, Draft, build_chat_map, build_msg_map
|
from ..types import (
|
||||||
from .messages import parse_message
|
AsyncList,
|
||||||
|
ChatLike,
|
||||||
|
Dialog,
|
||||||
|
Draft,
|
||||||
|
build_chat_map,
|
||||||
|
build_msg_map,
|
||||||
|
parse_message,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
@ -125,7 +132,6 @@ async def edit_draft(
|
||||||
message, entities = parse_message(
|
message, entities = parse_message(
|
||||||
text=text, markdown=markdown, html=html, allow_empty=False
|
text=text, markdown=markdown, html=html, allow_empty=False
|
||||||
)
|
)
|
||||||
assert isinstance(message, str)
|
|
||||||
|
|
||||||
result = await self(
|
result = await self(
|
||||||
functions.messages.save_draft(
|
functions.messages.save_draft(
|
||||||
|
|
|
@ -2,13 +2,13 @@ from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import urllib.parse
|
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, List, Optional, Union
|
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||||
|
|
||||||
from ...tl import abcs, functions, types
|
from ...tl import abcs, functions, types
|
||||||
from ..types import (
|
from ..types import (
|
||||||
|
AlbumBuilder,
|
||||||
AsyncList,
|
AsyncList,
|
||||||
ChatLike,
|
ChatLike,
|
||||||
File,
|
File,
|
||||||
|
@ -16,10 +16,14 @@ from ..types import (
|
||||||
Message,
|
Message,
|
||||||
OutFileLike,
|
OutFileLike,
|
||||||
OutWrapper,
|
OutWrapper,
|
||||||
|
)
|
||||||
|
from ..types import buttons as btns
|
||||||
|
from ..types import (
|
||||||
expand_stripped_size,
|
expand_stripped_size,
|
||||||
generate_random_id,
|
generate_random_id,
|
||||||
|
parse_message,
|
||||||
|
try_get_url_path,
|
||||||
)
|
)
|
||||||
from .messages import parse_message
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
@ -34,6 +38,10 @@ BIG_FILE_SIZE = 10 * 1024 * 1024
|
||||||
math_round = round
|
math_round = round
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_album(self: Client) -> AlbumBuilder:
|
||||||
|
return AlbumBuilder._create(client=self)
|
||||||
|
|
||||||
|
|
||||||
async def send_photo(
|
async def send_photo(
|
||||||
self: Client,
|
self: Client,
|
||||||
chat: ChatLike,
|
chat: ChatLike,
|
||||||
|
@ -41,12 +49,15 @@ async def send_photo(
|
||||||
*,
|
*,
|
||||||
size: Optional[int] = None,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
|
mime_type: Optional[str] = None,
|
||||||
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: Optional[str] = None,
|
||||||
caption_markdown: Optional[str] = None,
|
caption_markdown: Optional[str] = None,
|
||||||
caption_html: Optional[str] = None,
|
caption_html: Optional[str] = None,
|
||||||
|
reply_to: Optional[int] = None,
|
||||||
|
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
return await send_file(
|
return await send_file(
|
||||||
self,
|
self,
|
||||||
|
@ -54,12 +65,17 @@ async def send_photo(
|
||||||
file,
|
file,
|
||||||
size=size,
|
size=size,
|
||||||
name=name,
|
name=name,
|
||||||
|
mime_type="image/jpeg" # specific mime doesn't matter, only that it's image
|
||||||
|
if compress
|
||||||
|
else mime_type,
|
||||||
compress=compress,
|
compress=compress,
|
||||||
width=width,
|
width=width,
|
||||||
height=height,
|
height=height,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
caption_markdown=caption_markdown,
|
caption_markdown=caption_markdown,
|
||||||
caption_html=caption_html,
|
caption_html=caption_html,
|
||||||
|
reply_to=reply_to,
|
||||||
|
buttons=buttons,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,6 +83,7 @@ async def send_audio(
|
||||||
self: Client,
|
self: Client,
|
||||||
chat: ChatLike,
|
chat: ChatLike,
|
||||||
file: Union[str, Path, InFileLike, File],
|
file: Union[str, Path, InFileLike, File],
|
||||||
|
mime_type: Optional[str] = None,
|
||||||
*,
|
*,
|
||||||
size: Optional[int] = None,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
|
@ -77,6 +94,8 @@ async def send_audio(
|
||||||
caption: Optional[str] = None,
|
caption: Optional[str] = None,
|
||||||
caption_markdown: Optional[str] = None,
|
caption_markdown: Optional[str] = None,
|
||||||
caption_html: Optional[str] = None,
|
caption_html: Optional[str] = None,
|
||||||
|
reply_to: Optional[int] = None,
|
||||||
|
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
return await send_file(
|
return await send_file(
|
||||||
self,
|
self,
|
||||||
|
@ -84,6 +103,7 @@ async def send_audio(
|
||||||
file,
|
file,
|
||||||
size=size,
|
size=size,
|
||||||
name=name,
|
name=name,
|
||||||
|
mime_type=mime_type,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
voice=voice,
|
voice=voice,
|
||||||
title=title,
|
title=title,
|
||||||
|
@ -91,6 +111,8 @@ async def send_audio(
|
||||||
caption=caption,
|
caption=caption,
|
||||||
caption_markdown=caption_markdown,
|
caption_markdown=caption_markdown,
|
||||||
caption_html=caption_html,
|
caption_html=caption_html,
|
||||||
|
reply_to=reply_to,
|
||||||
|
buttons=buttons,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,41 +123,40 @@ async def send_video(
|
||||||
*,
|
*,
|
||||||
size: Optional[int] = None,
|
size: Optional[int] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
|
mime_type: Optional[str] = None,
|
||||||
duration: Optional[float] = None,
|
duration: Optional[float] = None,
|
||||||
width: Optional[int] = None,
|
width: Optional[int] = None,
|
||||||
height: Optional[int] = None,
|
height: Optional[int] = None,
|
||||||
round: bool = False,
|
round: bool = False,
|
||||||
supports_streaming: bool = False,
|
supports_streaming: bool = False,
|
||||||
|
muted: bool = False,
|
||||||
caption: Optional[str] = None,
|
caption: Optional[str] = None,
|
||||||
caption_markdown: Optional[str] = None,
|
caption_markdown: Optional[str] = None,
|
||||||
caption_html: Optional[str] = None,
|
caption_html: Optional[str] = None,
|
||||||
|
reply_to: Optional[int] = None,
|
||||||
|
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
return await send_file(
|
return await send_file(
|
||||||
self,
|
self,
|
||||||
chat,
|
chat,
|
||||||
file,
|
file,
|
||||||
size=size,
|
size=size,
|
||||||
|
mime_type=mime_type,
|
||||||
name=name,
|
name=name,
|
||||||
duration=duration,
|
duration=duration,
|
||||||
width=width,
|
width=width,
|
||||||
height=height,
|
height=height,
|
||||||
round=round,
|
round=round,
|
||||||
supports_streaming=supports_streaming,
|
supports_streaming=supports_streaming,
|
||||||
|
muted=muted,
|
||||||
caption=caption,
|
caption=caption,
|
||||||
caption_markdown=caption_markdown,
|
caption_markdown=caption_markdown,
|
||||||
caption_html=caption_html,
|
caption_html=caption_html,
|
||||||
|
reply_to=reply_to,
|
||||||
|
buttons=buttons,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def try_get_url_path(maybe_url: Union[str, Path, InFileLike]) -> Optional[str]:
|
|
||||||
if not isinstance(maybe_url, str):
|
|
||||||
return None
|
|
||||||
lowercase = maybe_url.lower()
|
|
||||||
if lowercase.startswith("http://") or lowercase.startswith("https://"):
|
|
||||||
return urllib.parse.urlparse(maybe_url).path
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def send_file(
|
async def send_file(
|
||||||
self: Client,
|
self: Client,
|
||||||
chat: ChatLike,
|
chat: ChatLike,
|
||||||
|
@ -160,15 +181,18 @@ async def send_file(
|
||||||
caption: Optional[str] = None,
|
caption: Optional[str] = None,
|
||||||
caption_markdown: Optional[str] = None,
|
caption_markdown: Optional[str] = None,
|
||||||
caption_html: Optional[str] = None,
|
caption_html: Optional[str] = None,
|
||||||
|
reply_to: Optional[int] = None,
|
||||||
|
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
message, entities = parse_message(
|
message, entities = parse_message(
|
||||||
text=caption, markdown=caption_markdown, html=caption_html, allow_empty=True
|
text=caption, markdown=caption_markdown, html=caption_html, allow_empty=True
|
||||||
)
|
)
|
||||||
assert isinstance(message, str)
|
|
||||||
|
|
||||||
# Re-send existing file.
|
# Re-send existing file.
|
||||||
if isinstance(file, File):
|
if isinstance(file, File):
|
||||||
return await do_send_file(self, chat, file._input_media, message, entities)
|
return await do_send_file(
|
||||||
|
self, chat, file._input_media, message, entities, reply_to, buttons
|
||||||
|
)
|
||||||
|
|
||||||
# URLs are handled early as they can't use any other attributes either.
|
# URLs are handled early as they can't use any other attributes either.
|
||||||
input_media: abcs.InputMedia
|
input_media: abcs.InputMedia
|
||||||
|
@ -190,23 +214,11 @@ async def send_file(
|
||||||
input_media = types.InputMediaDocumentExternal(
|
input_media = types.InputMediaDocumentExternal(
|
||||||
spoiler=False, url=file, ttl_seconds=None
|
spoiler=False, url=file, ttl_seconds=None
|
||||||
)
|
)
|
||||||
return await do_send_file(self, chat, input_media, message, entities)
|
return await do_send_file(
|
||||||
|
self, chat, input_media, message, entities, reply_to, buttons
|
||||||
|
)
|
||||||
|
|
||||||
# Paths are opened and closed by us. Anything else is *only* read, not closed.
|
input_file, name = await upload(self, file, size, name)
|
||||||
if isinstance(file, (str, Path)):
|
|
||||||
path = Path(file) if isinstance(file, str) else file
|
|
||||||
if size is None:
|
|
||||||
size = path.stat().st_size
|
|
||||||
if name is None:
|
|
||||||
name = path.name
|
|
||||||
with path.open("rb") as fd:
|
|
||||||
input_file = await upload(self, fd, size, name)
|
|
||||||
else:
|
|
||||||
if size is None:
|
|
||||||
raise ValueError("size must be set when sending file-like objects")
|
|
||||||
if name is None:
|
|
||||||
raise ValueError("name must be set when sending file-like objects")
|
|
||||||
input_file = await upload(self, file, size, name)
|
|
||||||
|
|
||||||
# Mime is mandatory for documents, but we also use it to determine whether to send as photo.
|
# Mime is mandatory for documents, but we also use it to determine whether to send as photo.
|
||||||
if mime_type is None:
|
if mime_type is None:
|
||||||
|
@ -249,7 +261,7 @@ async def send_file(
|
||||||
round_message=round,
|
round_message=round,
|
||||||
supports_streaming=supports_streaming,
|
supports_streaming=supports_streaming,
|
||||||
nosound=muted,
|
nosound=muted,
|
||||||
duration=int(math_round(duration)),
|
duration=duration,
|
||||||
w=width,
|
w=width,
|
||||||
h=height,
|
h=height,
|
||||||
preload_prefix_size=None,
|
preload_prefix_size=None,
|
||||||
|
@ -268,7 +280,9 @@ async def send_file(
|
||||||
ttl_seconds=None,
|
ttl_seconds=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await do_send_file(self, chat, input_media, message, entities)
|
return await do_send_file(
|
||||||
|
self, chat, input_media, message, entities, reply_to, buttons
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def do_send_file(
|
async def do_send_file(
|
||||||
|
@ -277,6 +291,8 @@ async def do_send_file(
|
||||||
input_media: abcs.InputMedia,
|
input_media: abcs.InputMedia,
|
||||||
message: str,
|
message: str,
|
||||||
entities: Optional[List[abcs.MessageEntity]],
|
entities: Optional[List[abcs.MessageEntity]],
|
||||||
|
reply_to: Optional[int],
|
||||||
|
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]],
|
||||||
) -> Message:
|
) -> Message:
|
||||||
peer = (await client._resolve_to_packed(chat))._to_input_peer()
|
peer = (await client._resolve_to_packed(chat))._to_input_peer()
|
||||||
random_id = generate_random_id()
|
random_id = generate_random_id()
|
||||||
|
@ -289,11 +305,15 @@ async def do_send_file(
|
||||||
noforwards=False,
|
noforwards=False,
|
||||||
update_stickersets_order=False,
|
update_stickersets_order=False,
|
||||||
peer=peer,
|
peer=peer,
|
||||||
reply_to=None,
|
reply_to=types.InputReplyToMessage(
|
||||||
|
reply_to_msg_id=reply_to, top_msg_id=None
|
||||||
|
)
|
||||||
|
if reply_to
|
||||||
|
else None,
|
||||||
media=input_media,
|
media=input_media,
|
||||||
message=message,
|
message=message,
|
||||||
random_id=random_id,
|
random_id=random_id,
|
||||||
reply_markup=None,
|
reply_markup=btns.build_keyboard(buttons),
|
||||||
entities=entities,
|
entities=entities,
|
||||||
schedule_date=None,
|
schedule_date=None,
|
||||||
send_as=None,
|
send_as=None,
|
||||||
|
@ -304,6 +324,31 @@ async def do_send_file(
|
||||||
|
|
||||||
|
|
||||||
async def upload(
|
async def upload(
|
||||||
|
client: Client,
|
||||||
|
file: Union[str, Path, InFileLike],
|
||||||
|
size: Optional[int],
|
||||||
|
name: Optional[str],
|
||||||
|
) -> Tuple[abcs.InputFile, str]:
|
||||||
|
# Paths are opened and closed by us. Anything else is *only* read, not closed.
|
||||||
|
if isinstance(file, (str, Path)):
|
||||||
|
path = Path(file) if isinstance(file, str) else file
|
||||||
|
if size is None:
|
||||||
|
size = path.stat().st_size
|
||||||
|
if name is None:
|
||||||
|
name = path.name
|
||||||
|
with path.open("rb") as fd:
|
||||||
|
return await do_upload(client, fd, size, name), name
|
||||||
|
else:
|
||||||
|
if size is None:
|
||||||
|
raise ValueError("size must be set when sending file-like objects")
|
||||||
|
if name is None:
|
||||||
|
name = getattr(file, "name", None)
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise ValueError("name must be set when sending file-like objects")
|
||||||
|
return await do_upload(client, file, size, name), name
|
||||||
|
|
||||||
|
|
||||||
|
async def do_upload(
|
||||||
client: Client,
|
client: Client,
|
||||||
fd: InFileLike,
|
fd: InFileLike,
|
||||||
size: int,
|
size: int,
|
||||||
|
|
|
@ -2,85 +2,18 @@ from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Self, Tuple, Union
|
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Self, Union
|
||||||
|
|
||||||
from ...session import PackedChat
|
from ...session import PackedChat
|
||||||
from ...tl import abcs, functions, types
|
from ...tl import abcs, functions, types
|
||||||
from ..parsers import parse_html_message, parse_markdown_message
|
from ..types import AsyncList, Chat, ChatLike, Message, build_chat_map
|
||||||
from ..types import (
|
from ..types import buttons as btns
|
||||||
AsyncList,
|
from ..types import generate_random_id, parse_message, peer_id
|
||||||
Chat,
|
|
||||||
ChatLike,
|
|
||||||
Message,
|
|
||||||
build_chat_map,
|
|
||||||
buttons,
|
|
||||||
generate_random_id,
|
|
||||||
peer_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
|
||||||
|
|
||||||
def parse_message(
|
|
||||||
*,
|
|
||||||
text: Optional[Union[str, Message]],
|
|
||||||
markdown: Optional[str],
|
|
||||||
html: Optional[str],
|
|
||||||
allow_empty: bool,
|
|
||||||
) -> Tuple[Union[str, Message], Optional[List[abcs.MessageEntity]]]:
|
|
||||||
cnt = sum((text is not None, markdown is not None, html is not None))
|
|
||||||
if cnt != 1:
|
|
||||||
if cnt == 0 and allow_empty:
|
|
||||||
return "", None
|
|
||||||
raise ValueError("must specify exactly one of text, markdown or html")
|
|
||||||
|
|
||||||
if text is not None:
|
|
||||||
parsed, entities = text, None
|
|
||||||
elif markdown is not None:
|
|
||||||
parsed, entities = parse_markdown_message(markdown)
|
|
||||||
elif html is not None:
|
|
||||||
parsed, entities = parse_html_message(html)
|
|
||||||
else:
|
|
||||||
raise RuntimeError("unexpected case")
|
|
||||||
|
|
||||||
return parsed, entities or None
|
|
||||||
|
|
||||||
|
|
||||||
def build_keyboard(
|
|
||||||
btns: Optional[Union[List[buttons.Button], List[List[buttons.Button]]]]
|
|
||||||
) -> Optional[abcs.ReplyMarkup]:
|
|
||||||
# list[button] -> list[list[button]]
|
|
||||||
# This does allow for "invalid" inputs (mixing lists and non-lists), but that's acceptable.
|
|
||||||
buttons_lists_iter = (
|
|
||||||
button if isinstance(button, list) else [button] for button in (btns or [])
|
|
||||||
)
|
|
||||||
# Remove empty rows (also making it easy to check if all-empty).
|
|
||||||
buttons_lists = [bs for bs in buttons_lists_iter if bs]
|
|
||||||
|
|
||||||
if not buttons_lists:
|
|
||||||
return None
|
|
||||||
|
|
||||||
rows: List[abcs.KeyboardButtonRow] = [
|
|
||||||
types.KeyboardButtonRow(buttons=[btn._raw for btn in btns])
|
|
||||||
for btns in buttons_lists
|
|
||||||
]
|
|
||||||
|
|
||||||
# Guaranteed to have at least one, first one used to check if it's inline.
|
|
||||||
# If the user mixed inline with non-inline, Telegram will complain.
|
|
||||||
if isinstance(buttons_lists[0][0], buttons.InlineButton):
|
|
||||||
return types.ReplyInlineMarkup(rows=rows)
|
|
||||||
else:
|
|
||||||
return types.ReplyKeyboardMarkup(
|
|
||||||
resize=False,
|
|
||||||
single_use=False,
|
|
||||||
selective=False,
|
|
||||||
persistent=False,
|
|
||||||
rows=rows,
|
|
||||||
placeholder=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_message(
|
async def send_message(
|
||||||
self: Client,
|
self: Client,
|
||||||
chat: ChatLike,
|
chat: ChatLike,
|
||||||
|
@ -90,16 +23,39 @@ async def send_message(
|
||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
link_preview: bool = False,
|
link_preview: bool = False,
|
||||||
reply_to: Optional[int] = None,
|
reply_to: Optional[int] = None,
|
||||||
buttons: Optional[Union[List[buttons.Button], List[List[buttons.Button]]]] = None,
|
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
packed = await self._resolve_to_packed(chat)
|
packed = await self._resolve_to_packed(chat)
|
||||||
peer = packed._to_input_peer()
|
peer = packed._to_input_peer()
|
||||||
message, entities = parse_message(
|
|
||||||
text=text, markdown=markdown, html=html, allow_empty=False
|
|
||||||
)
|
|
||||||
random_id = generate_random_id()
|
random_id = generate_random_id()
|
||||||
result = await self(
|
|
||||||
functions.messages.send_message(
|
if isinstance(text, Message):
|
||||||
|
message = text.text or ""
|
||||||
|
request = functions.messages.send_message(
|
||||||
|
no_webpage=not text.link_preview,
|
||||||
|
silent=text.silent,
|
||||||
|
background=False,
|
||||||
|
clear_draft=False,
|
||||||
|
noforwards=not text.can_forward,
|
||||||
|
update_stickersets_order=False,
|
||||||
|
peer=peer,
|
||||||
|
reply_to=types.InputReplyToMessage(
|
||||||
|
reply_to_msg_id=text.replied_message_id, top_msg_id=None
|
||||||
|
)
|
||||||
|
if text.replied_message_id
|
||||||
|
else None,
|
||||||
|
message=message,
|
||||||
|
random_id=random_id,
|
||||||
|
reply_markup=getattr(text._raw, "reply_markup", None),
|
||||||
|
entities=getattr(text._raw, "entities", None) or None,
|
||||||
|
schedule_date=None,
|
||||||
|
send_as=None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message, entities = parse_message(
|
||||||
|
text=text, markdown=markdown, html=html, allow_empty=False
|
||||||
|
)
|
||||||
|
request = functions.messages.send_message(
|
||||||
no_webpage=not link_preview,
|
no_webpage=not link_preview,
|
||||||
silent=False,
|
silent=False,
|
||||||
background=False,
|
background=False,
|
||||||
|
@ -114,33 +70,13 @@ async def send_message(
|
||||||
else None,
|
else None,
|
||||||
message=message,
|
message=message,
|
||||||
random_id=random_id,
|
random_id=random_id,
|
||||||
reply_markup=build_keyboard(buttons),
|
reply_markup=btns.build_keyboard(buttons),
|
||||||
entities=entities,
|
entities=entities,
|
||||||
schedule_date=None,
|
schedule_date=None,
|
||||||
send_as=None,
|
send_as=None,
|
||||||
)
|
)
|
||||||
if isinstance(message, str)
|
|
||||||
else functions.messages.send_message(
|
result = await self(request)
|
||||||
no_webpage=not message.link_preview,
|
|
||||||
silent=message.silent,
|
|
||||||
background=False,
|
|
||||||
clear_draft=False,
|
|
||||||
noforwards=not message.can_forward,
|
|
||||||
update_stickersets_order=False,
|
|
||||||
peer=peer,
|
|
||||||
reply_to=types.InputReplyToMessage(
|
|
||||||
reply_to_msg_id=message.replied_message_id, top_msg_id=None
|
|
||||||
)
|
|
||||||
if message.replied_message_id
|
|
||||||
else None,
|
|
||||||
message=message.text or "",
|
|
||||||
random_id=random_id,
|
|
||||||
reply_markup=getattr(message._raw, "reply_markup", None),
|
|
||||||
entities=getattr(message._raw, "entities", None) or None,
|
|
||||||
schedule_date=None,
|
|
||||||
send_as=None,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if isinstance(result, types.UpdateShortSentMessage):
|
if isinstance(result, types.UpdateShortSentMessage):
|
||||||
return Message._from_defaults(
|
return Message._from_defaults(
|
||||||
self,
|
self,
|
||||||
|
@ -161,7 +97,7 @@ async def send_message(
|
||||||
if reply_to
|
if reply_to
|
||||||
else None,
|
else None,
|
||||||
date=result.date,
|
date=result.date,
|
||||||
message=message if isinstance(message, str) else (message.text or ""),
|
message=message,
|
||||||
media=result.media,
|
media=result.media,
|
||||||
entities=result.entities,
|
entities=result.entities,
|
||||||
ttl_period=result.ttl_period,
|
ttl_period=result.ttl_period,
|
||||||
|
@ -184,7 +120,6 @@ async def edit_message(
|
||||||
message, entities = parse_message(
|
message, entities = parse_message(
|
||||||
text=text, markdown=markdown, html=html, allow_empty=False
|
text=text, markdown=markdown, html=html, allow_empty=False
|
||||||
)
|
)
|
||||||
assert isinstance(message, str)
|
|
||||||
return self._build_message_map(
|
return self._build_message_map(
|
||||||
await self(
|
await self(
|
||||||
functions.messages.edit_message(
|
functions.messages.edit_message(
|
||||||
|
|
|
@ -154,5 +154,5 @@ async def dispatch_next(client: Client) -> None:
|
||||||
for handler, filter in handlers:
|
for handler, filter in handlers:
|
||||||
if not filter or filter(event):
|
if not filter or filter(event):
|
||||||
ret = await handler(event)
|
ret = await handler(event)
|
||||||
if ret is Continue or client._shortcircuit_handlers:
|
if ret is not Continue or client._shortcircuit_handlers:
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, List, Optional, Sequence
|
||||||
|
|
||||||
from ...mtproto import RpcError
|
from ...mtproto import RpcError
|
||||||
from ...session import PackedChat, PackedType
|
from ...session import PackedChat, PackedType
|
||||||
|
@ -13,6 +13,7 @@ from ..types import (
|
||||||
Group,
|
Group,
|
||||||
User,
|
User,
|
||||||
build_chat_map,
|
build_chat_map,
|
||||||
|
expand_peer,
|
||||||
peer_id,
|
peer_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -73,12 +74,51 @@ async def resolve_username(self: Client, username: str) -> Chat:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
|
async def get_chats(self: Client, chats: Sequence[ChatLike]) -> List[Chat]:
|
||||||
|
packed_chats: List[PackedChat] = []
|
||||||
|
input_users: List[types.InputUser] = []
|
||||||
|
input_chats: List[int] = []
|
||||||
|
input_channels: List[types.InputChannel] = []
|
||||||
|
|
||||||
|
for chat in chats:
|
||||||
|
packed = await resolve_to_packed(self, chat)
|
||||||
|
if packed.is_user():
|
||||||
|
input_users.append(packed._to_input_user())
|
||||||
|
elif packed.is_chat():
|
||||||
|
input_chats.append(packed.id)
|
||||||
|
else:
|
||||||
|
input_channels.append(packed._to_input_channel())
|
||||||
|
|
||||||
|
users = (
|
||||||
|
(await self(functions.users.get_users(id=input_users))) if input_users else []
|
||||||
|
)
|
||||||
|
groups = (
|
||||||
|
(await self(functions.messages.get_chats(id=input_chats)))
|
||||||
|
if input_chats
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
assert isinstance(groups, types.messages.Chats)
|
||||||
|
channels = (
|
||||||
|
(await self(functions.channels.get_channels(id=input_channels)))
|
||||||
|
if input_channels
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
assert isinstance(channels, types.messages.Chats)
|
||||||
|
|
||||||
|
chat_map = build_chat_map(self, users, groups.chats + channels.chats)
|
||||||
|
return [
|
||||||
|
chat_map.get(chat.id)
|
||||||
|
or expand_peer(self, chat._to_peer(), broadcast=chat.ty == PackedType.BROADCAST)
|
||||||
|
for chat in packed_chats
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_to_packed(client: Client, chat: ChatLike) -> PackedChat:
|
||||||
if isinstance(chat, PackedChat):
|
if isinstance(chat, PackedChat):
|
||||||
return chat
|
return chat
|
||||||
|
|
||||||
if isinstance(chat, (User, Group, Channel)):
|
if isinstance(chat, (User, Group, Channel)):
|
||||||
packed = chat.pack() or self._chat_hashes.get(chat.id)
|
packed = chat.pack() or client._chat_hashes.get(chat.id)
|
||||||
if packed is not None:
|
if packed is not None:
|
||||||
return packed
|
return packed
|
||||||
|
|
||||||
|
@ -96,11 +136,11 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
|
||||||
if isinstance(chat, types.InputPeerEmpty):
|
if isinstance(chat, types.InputPeerEmpty):
|
||||||
raise ValueError("Cannot resolve chat")
|
raise ValueError("Cannot resolve chat")
|
||||||
elif isinstance(chat, types.InputPeerSelf):
|
elif isinstance(chat, types.InputPeerSelf):
|
||||||
if not self._session.user:
|
if not client._session.user:
|
||||||
raise ValueError("Cannot resolve chat")
|
raise ValueError("Cannot resolve chat")
|
||||||
return PackedChat(
|
return PackedChat(
|
||||||
ty=PackedType.BOT if self._session.user.bot else PackedType.USER,
|
ty=PackedType.BOT if client._session.user.bot else PackedType.USER,
|
||||||
id=self._chat_hashes.self_id,
|
id=client._chat_hashes.self_id,
|
||||||
access_hash=0,
|
access_hash=0,
|
||||||
)
|
)
|
||||||
elif isinstance(chat, types.InputPeerChat):
|
elif isinstance(chat, types.InputPeerChat):
|
||||||
|
@ -130,9 +170,9 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
|
||||||
|
|
||||||
if isinstance(chat, str):
|
if isinstance(chat, str):
|
||||||
if chat.startswith("+"):
|
if chat.startswith("+"):
|
||||||
resolved = await resolve_phone(self, chat)
|
resolved = await resolve_phone(client, chat)
|
||||||
elif chat == "me":
|
elif chat == "me":
|
||||||
if me := self._session.user:
|
if me := client._session.user:
|
||||||
return PackedChat(
|
return PackedChat(
|
||||||
ty=PackedType.BOT if me.bot else PackedType.USER,
|
ty=PackedType.BOT if me.bot else PackedType.USER,
|
||||||
id=me.id,
|
id=me.id,
|
||||||
|
@ -141,13 +181,13 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
|
||||||
else:
|
else:
|
||||||
resolved = None
|
resolved = None
|
||||||
else:
|
else:
|
||||||
resolved = await resolve_username(self, username=chat)
|
resolved = await resolve_username(client, username=chat)
|
||||||
|
|
||||||
if resolved and (packed := resolved.pack()) is not None:
|
if resolved and (packed := resolved.pack()) is not None:
|
||||||
return packed
|
return packed
|
||||||
|
|
||||||
if isinstance(chat, int):
|
if isinstance(chat, int):
|
||||||
packed = self._chat_hashes.get(chat)
|
packed = client._chat_hashes.get(chat)
|
||||||
if packed is None:
|
if packed is None:
|
||||||
raise ValueError("Cannot resolve chat")
|
raise ValueError("Cannot resolve chat")
|
||||||
return packed
|
return packed
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from .combinators import All, Any, Filter, Not
|
from .combinators import All, Any, Filter, Not
|
||||||
from .common import Chats, Senders
|
from .common import Chats, ChatType, Senders
|
||||||
from .messages import Command, Forward, Incoming, Media, Outgoing, Reply, Text, TextOnly
|
from .messages import Command, Forward, Incoming, Media, Outgoing, Reply, Text
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"All",
|
"All",
|
||||||
|
@ -8,6 +8,7 @@ __all__ = [
|
||||||
"Filter",
|
"Filter",
|
||||||
"Not",
|
"Not",
|
||||||
"Chats",
|
"Chats",
|
||||||
|
"ChatType",
|
||||||
"Senders",
|
"Senders",
|
||||||
"Command",
|
"Command",
|
||||||
"Forward",
|
"Forward",
|
||||||
|
@ -16,5 +17,4 @@ __all__ = [
|
||||||
"Outgoing",
|
"Outgoing",
|
||||||
"Reply",
|
"Reply",
|
||||||
"Text",
|
"Text",
|
||||||
"TextOnly",
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import abc
|
import abc
|
||||||
import typing
|
import typing
|
||||||
from typing import Callable, Tuple
|
from typing import Callable, Tuple, TypeAlias
|
||||||
|
|
||||||
from ..event import Event
|
from ..event import Event
|
||||||
|
|
||||||
Filter = Callable[[Event], bool]
|
Filter: TypeAlias = Callable[[Event], bool]
|
||||||
|
|
||||||
|
|
||||||
class Combinable(abc.ABC):
|
class Combinable(abc.ABC):
|
||||||
|
@ -48,11 +48,12 @@ class Any(Combinable):
|
||||||
"""
|
"""
|
||||||
Combine multiple filters, returning :data:`True` if any of the filters pass.
|
Combine multiple filters, returning :data:`True` if any of the filters pass.
|
||||||
|
|
||||||
When either filter is *combinable*, you can use the ``|`` operator instead.
|
When either filter is :class:`~telethon._impl.client.events.filters.combinators.Combinable`,
|
||||||
|
you can use the ``|`` operator instead.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from telethon.filters import Any, Command
|
from telethon.events.filters import Any, Command
|
||||||
|
|
||||||
@bot.on(events.NewMessage, Any(Command('/start'), Command('/help')))
|
@bot.on(events.NewMessage, Any(Command('/start'), Command('/help')))
|
||||||
async def handler(event): ...
|
async def handler(event): ...
|
||||||
|
@ -87,11 +88,12 @@ class All(Combinable):
|
||||||
"""
|
"""
|
||||||
Combine multiple filters, returning :data:`True` if all of the filters pass.
|
Combine multiple filters, returning :data:`True` if all of the filters pass.
|
||||||
|
|
||||||
When either filter is *combinable*, you can use the ``&`` operator instead.
|
When either filter is :class:`~telethon._impl.client.events.filters.combinators.Combinable`,
|
||||||
|
you can use the ``&`` operator instead.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from telethon.filters import All, Command, Text
|
from telethon.events.filters import All, Command, Text
|
||||||
|
|
||||||
@bot.on(events.NewMessage, All(Command('/start'), Text(r'\bdata:\w+')))
|
@bot.on(events.NewMessage, All(Command('/start'), Text(r'\bdata:\w+')))
|
||||||
async def handler(event): ...
|
async def handler(event): ...
|
||||||
|
@ -126,11 +128,12 @@ class Not(Combinable):
|
||||||
"""
|
"""
|
||||||
Negate the output of a single filter, returning :data:`True` if the nested filter does *not* pass.
|
Negate the output of a single filter, returning :data:`True` if the nested filter does *not* pass.
|
||||||
|
|
||||||
When the filter is *combinable*, you can use the ``~`` operator instead.
|
When the filter is :class:`~telethon._impl.client.events.filters.combinators.Combinable`,
|
||||||
|
you can use the ``~`` operator instead.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from telethon.filters import All, Command
|
from telethon.events.filters import All, Command
|
||||||
|
|
||||||
@bot.on(events.NewMessage, Not(Command('/start'))
|
@bot.on(events.NewMessage, Not(Command('/start'))
|
||||||
async def handler(event): ...
|
async def handler(event): ...
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Literal, Sequence, Tuple, Type, Union
|
from typing import Sequence, Set, Type, Union
|
||||||
|
|
||||||
from ...types import Channel, Group, User
|
from ...types import Channel, Group, User
|
||||||
from ..event import Event
|
from ..event import Event
|
||||||
|
@ -8,20 +8,21 @@ from .combinators import Combinable
|
||||||
class Chats(Combinable):
|
class Chats(Combinable):
|
||||||
"""
|
"""
|
||||||
Filter by ``event.chat.id``, if the event has a chat.
|
Filter by ``event.chat.id``, if the event has a chat.
|
||||||
|
|
||||||
|
:param chat_ids: The chat identifiers to filter on.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_chats",)
|
__slots__ = ("_chats",)
|
||||||
|
|
||||||
def __init__(self, chat_id: Union[int, Sequence[int]], *chat_ids: int) -> None:
|
def __init__(self, chat_ids: Sequence[int]) -> None:
|
||||||
self._chats = {chat_id} if isinstance(chat_id, int) else set(chat_id)
|
self._chats = set(chat_ids)
|
||||||
self._chats.update(chat_ids)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chat_ids(self) -> Tuple[int, ...]:
|
def chat_ids(self) -> Set[int]:
|
||||||
"""
|
"""
|
||||||
The chat identifiers this filter is filtering on.
|
A copy of the set of chat identifiers this filter is filtering on.
|
||||||
"""
|
"""
|
||||||
return tuple(self._chats)
|
return set(self._chats)
|
||||||
|
|
||||||
def __call__(self, event: Event) -> bool:
|
def __call__(self, event: Event) -> bool:
|
||||||
chat = getattr(event, "chat", None)
|
chat = getattr(event, "chat", None)
|
||||||
|
@ -32,20 +33,21 @@ class Chats(Combinable):
|
||||||
class Senders(Combinable):
|
class Senders(Combinable):
|
||||||
"""
|
"""
|
||||||
Filter by ``event.sender.id``, if the event has a sender.
|
Filter by ``event.sender.id``, if the event has a sender.
|
||||||
|
|
||||||
|
:param sender_ids: The sender identifiers to filter on.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_senders",)
|
__slots__ = ("_senders",)
|
||||||
|
|
||||||
def __init__(self, sender_id: Union[int, Sequence[int]], *sender_ids: int) -> None:
|
def __init__(self, sender_ids: Sequence[int]) -> None:
|
||||||
self._senders = {sender_id} if isinstance(sender_id, int) else set(sender_id)
|
self._senders = set(sender_ids)
|
||||||
self._senders.update(sender_ids)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sender_ids(self) -> Tuple[int, ...]:
|
def sender_ids(self) -> Set[int]:
|
||||||
"""
|
"""
|
||||||
The sender identifiers this filter is filtering on.
|
A copy of the set of sender identifiers this filter is filtering on.
|
||||||
"""
|
"""
|
||||||
return tuple(self._senders)
|
return set(self._senders)
|
||||||
|
|
||||||
def __call__(self, event: Event) -> bool:
|
def __call__(self, event: Event) -> bool:
|
||||||
sender = getattr(event, "sender", None)
|
sender = getattr(event, "sender", None)
|
||||||
|
@ -55,37 +57,38 @@ class Senders(Combinable):
|
||||||
|
|
||||||
class ChatType(Combinable):
|
class ChatType(Combinable):
|
||||||
"""
|
"""
|
||||||
Filter by chat type, either ``'user'``, ``'group'`` or ``'broadcast'``.
|
Filter by chat type using :func:`isinstance`.
|
||||||
|
|
||||||
|
:param type: The chat type to filter on.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import events
|
||||||
|
from telethon.events import filters
|
||||||
|
from telethon.types import Channel
|
||||||
|
|
||||||
|
# Handle only messages from broadcast channels
|
||||||
|
@client.on(events.NewMessage, filters.ChatType(Channel))
|
||||||
|
async def handler(event):
|
||||||
|
print(event.text)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_type",)
|
__slots__ = ("_type",)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
type: Union[Literal["user"], Literal["group"], Literal["broadcast"]],
|
type: Type[Union[User, Group, Channel]],
|
||||||
) -> None:
|
) -> None:
|
||||||
if type == "user":
|
self._type = type
|
||||||
self._type: Union[Type[User], Type[Group], Type[Channel]] = User
|
|
||||||
elif type == "group":
|
|
||||||
self._type = Group
|
|
||||||
elif type == "broadcast":
|
|
||||||
self._type = Channel
|
|
||||||
else:
|
|
||||||
raise TypeError(f"unrecognised chat type: {type}")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self) -> Union[Literal["user"], Literal["group"], Literal["broadcast"]]:
|
def type(self) -> Type[Union[User, Group, Channel]]:
|
||||||
"""
|
"""
|
||||||
The chat type this filter is filtering on.
|
The chat type this filter is filtering on.
|
||||||
"""
|
"""
|
||||||
if self._type == User:
|
return self._type
|
||||||
return "user"
|
|
||||||
elif self._type == Group:
|
|
||||||
return "group"
|
|
||||||
elif self._type == Channel:
|
|
||||||
return "broadcast"
|
|
||||||
else:
|
|
||||||
raise RuntimeError("unexpected case")
|
|
||||||
|
|
||||||
def __call__(self, event: Event) -> bool:
|
def __call__(self, event: Event) -> bool:
|
||||||
sender = getattr(event, "chat", None)
|
sender = getattr(event, "chat", None)
|
||||||
|
|
|
@ -21,7 +21,9 @@ class Text(Combinable):
|
||||||
you need to manually perform the check inside the handler instead.
|
you need to manually perform the check inside the handler instead.
|
||||||
|
|
||||||
Note that the caption text in messages with media is also searched.
|
Note that the caption text in messages with media is also searched.
|
||||||
If you want to filter based on media, use :class:`TextOnly` or :class:`Media`.
|
If you want to filter based on media, use :class:`Media`.
|
||||||
|
|
||||||
|
:param regexp: The regular expression to :func:`re.search` with on the text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_pattern",)
|
__slots__ = ("_pattern",)
|
||||||
|
@ -43,6 +45,8 @@ class Command(Combinable):
|
||||||
filter ``Command('/help')`` will match both ``"/help"`` and ``"/help@bot"``, but not
|
filter ``Command('/help')`` will match both ``"/help"`` and ``"/help@bot"``, but not
|
||||||
``"/list"`` or ``"/help@other"``.
|
``"/list"`` or ``"/help@other"``.
|
||||||
|
|
||||||
|
:param command: The command to match on.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
The leading forward-slash is not automatically added!
|
The leading forward-slash is not automatically added!
|
||||||
|
@ -58,7 +62,7 @@ class Command(Combinable):
|
||||||
__slots__ = ("_cmd", "_username")
|
__slots__ = ("_cmd", "_username")
|
||||||
|
|
||||||
def __init__(self, command: str) -> None:
|
def __init__(self, command: str) -> None:
|
||||||
if re.match(r"\s", command):
|
if re.search(r"\s", command):
|
||||||
raise ValueError(f"command cannot contain spaces: {command}")
|
raise ValueError(f"command cannot contain spaces: {command}")
|
||||||
|
|
||||||
self._cmd = command
|
self._cmd = command
|
||||||
|
@ -87,11 +91,7 @@ class Command(Combinable):
|
||||||
|
|
||||||
class Incoming(Combinable):
|
class Incoming(Combinable):
|
||||||
"""
|
"""
|
||||||
Filter by ``event.incoming``, that is, messages sent from others to the
|
Filter by ``event.incoming``, that is, messages sent from others to the logged-in account.
|
||||||
logged-in account.
|
|
||||||
|
|
||||||
This is not a reliable way to check that the update was not produced by
|
|
||||||
the logged-in account in broadcast channels.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
@ -102,11 +102,9 @@ class Incoming(Combinable):
|
||||||
|
|
||||||
class Outgoing(Combinable):
|
class Outgoing(Combinable):
|
||||||
"""
|
"""
|
||||||
Filter by ``event.outgoing``, that is, messages sent from others to the
|
Filter by ``event.outgoing``, that is, messages sent from the logged-in account.
|
||||||
logged-in account.
|
|
||||||
|
|
||||||
This is not a reliable way to check that the update was not produced by
|
This is not a reliable way to check that the update was not produced by the logged-in account in broadcast channels.
|
||||||
the logged-in account in broadcast channels.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
@ -117,32 +115,24 @@ class Outgoing(Combinable):
|
||||||
|
|
||||||
class Forward(Combinable):
|
class Forward(Combinable):
|
||||||
"""
|
"""
|
||||||
Filter by ``event.forward``.
|
Filter by ``event.forward_info``, that is, messages that have been forwarded from elsewhere.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def __call__(self, event: Event) -> bool:
|
def __call__(self, event: Event) -> bool:
|
||||||
return getattr(event, "forward", None) is not None
|
return getattr(event, "forward_info", None) is not None
|
||||||
|
|
||||||
|
|
||||||
class Reply(Combinable):
|
class Reply(Combinable):
|
||||||
"""
|
"""
|
||||||
Filter by ``event.reply``.
|
Filter by ``event.replied_message_id``, that is, messages which are a reply to another message.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def __call__(self, event: Event) -> bool:
|
def __call__(self, event: Event) -> bool:
|
||||||
return getattr(event, "reply", None) is not None
|
return getattr(event, "replied_message_id", None) is not None
|
||||||
|
|
||||||
|
|
||||||
class TextOnly(Combinable):
|
|
||||||
"""
|
|
||||||
Filter by messages with some text and no media.
|
|
||||||
|
|
||||||
Note that link previews are only considered media if they have a photo or document.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Media(Combinable):
|
class Media(Combinable):
|
||||||
|
@ -156,6 +146,10 @@ class Media(Combinable):
|
||||||
When you specify one or more media types, *only* those types will be considered.
|
When you specify one or more media types, *only* those types will be considered.
|
||||||
|
|
||||||
You can use literal strings or the constants defined by the filter.
|
You can use literal strings or the constants defined by the filter.
|
||||||
|
|
||||||
|
:param types:
|
||||||
|
The media types to filter on.
|
||||||
|
This is all of them if none are specified.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PHOTO = "photo"
|
PHOTO = "photo"
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Self
|
from typing import TYPE_CHECKING, Dict, List, Optional, Self, Union
|
||||||
|
|
||||||
from ...tl import abcs, types
|
from ...tl import abcs, types
|
||||||
from ..types import Chat, Message
|
from ..types import Chat, Message, expand_peer, peer_id
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -14,6 +14,8 @@ class NewMessage(Event, Message):
|
||||||
"""
|
"""
|
||||||
Occurs when a new message is sent or received.
|
Occurs when a new message is sent or received.
|
||||||
|
|
||||||
|
This event can be treated as the :class:`~telethon.types.Message` itself.
|
||||||
|
|
||||||
.. caution::
|
.. caution::
|
||||||
|
|
||||||
Messages sent with the :class:`~telethon.Client` are also caught,
|
Messages sent with the :class:`~telethon.Client` are also caught,
|
||||||
|
@ -39,6 +41,8 @@ class NewMessage(Event, Message):
|
||||||
class MessageEdited(Event, Message):
|
class MessageEdited(Event, Message):
|
||||||
"""
|
"""
|
||||||
Occurs when a new message is sent or received.
|
Occurs when a new message is sent or received.
|
||||||
|
|
||||||
|
This event can be treated as the :class:`~telethon.types.Message` itself.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -80,32 +84,96 @@ class MessageDeleted(Event):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message_ids(self) -> List[int]:
|
||||||
|
"""
|
||||||
|
The message identifiers of the messages that were deleted.
|
||||||
|
"""
|
||||||
|
return self._msg_ids
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel_id(self) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
The channel identifier of the supergroup or broadcast channel where the messages were deleted.
|
||||||
|
|
||||||
|
This will be :data:`None` if the messages were deleted anywhere else.
|
||||||
|
"""
|
||||||
|
return self._channel_id
|
||||||
|
|
||||||
|
|
||||||
class MessageRead(Event):
|
class MessageRead(Event):
|
||||||
"""
|
"""
|
||||||
Occurs both when your messages are read by others, and when you read messages.
|
Occurs both when your messages are read by others, and when you read messages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, peer: abcs.Peer, max_id: int, out: bool) -> None:
|
def __init__(
|
||||||
self._peer = peer
|
self,
|
||||||
self._max_id = max_id
|
client: Client,
|
||||||
self._out = out
|
update: Union[
|
||||||
|
types.UpdateReadHistoryInbox,
|
||||||
|
types.UpdateReadHistoryOutbox,
|
||||||
|
types.UpdateReadChannelInbox,
|
||||||
|
types.UpdateReadChannelOutbox,
|
||||||
|
],
|
||||||
|
chat_map: Dict[int, Chat],
|
||||||
|
) -> None:
|
||||||
|
self._client = client
|
||||||
|
self._raw = update
|
||||||
|
self._chat_map = chat_map
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _try_from_update(
|
def _try_from_update(
|
||||||
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
||||||
) -> Optional[Self]:
|
) -> Optional[Self]:
|
||||||
if isinstance(update, types.UpdateReadHistoryInbox):
|
if isinstance(
|
||||||
return cls._create(update.peer, update.max_id, False)
|
update,
|
||||||
elif isinstance(update, types.UpdateReadHistoryOutbox):
|
(
|
||||||
return cls._create(update.peer, update.max_id, True)
|
types.UpdateReadHistoryInbox,
|
||||||
elif isinstance(update, types.UpdateReadChannelInbox):
|
types.UpdateReadHistoryOutbox,
|
||||||
return cls._create(
|
types.UpdateReadChannelInbox,
|
||||||
types.PeerChannel(channel_id=update.channel_id), update.max_id, False
|
types.UpdateReadChannelOutbox,
|
||||||
)
|
),
|
||||||
elif isinstance(update, types.UpdateReadChannelOutbox):
|
):
|
||||||
return cls._create(
|
return cls._create(client, update, chat_map)
|
||||||
types.PeerChannel(channel_id=update.channel_id), update.max_id, True
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _peer(self) -> abcs.Peer:
|
||||||
|
if isinstance(
|
||||||
|
self._raw, (types.UpdateReadHistoryInbox, types.UpdateReadHistoryOutbox)
|
||||||
|
):
|
||||||
|
return self._raw.peer
|
||||||
|
else:
|
||||||
|
return types.PeerChannel(channel_id=self._raw.channel_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chat(self) -> Chat:
|
||||||
|
"""
|
||||||
|
The :term:`chat` when the messages were read.
|
||||||
|
"""
|
||||||
|
peer = self._peer()
|
||||||
|
pid = peer_id(peer)
|
||||||
|
if pid not in self._chat_map:
|
||||||
|
self._chat_map[pid] = expand_peer(
|
||||||
|
self._client, peer, broadcast=getattr(self._raw, "post", None)
|
||||||
|
)
|
||||||
|
return self._chat_map[pid]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_message_id_read(self) -> int:
|
||||||
|
"""
|
||||||
|
The highest message identifier of the messages that have been marked as read.
|
||||||
|
|
||||||
|
In other words, messages with an identifier below or equal (``<=``) to this value are considered read.
|
||||||
|
Messages with an identifier higher (``>``) to this value are considered unread.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
if message.id <= event.max_message_id_read:
|
||||||
|
print('message is marked as read')
|
||||||
|
else:
|
||||||
|
print('message is not yet marked as read')
|
||||||
|
"""
|
||||||
|
return self._raw.max_id
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from .admin_right import AdminRight
|
from .admin_right import AdminRight
|
||||||
|
from .album_builder import AlbumBuilder
|
||||||
from .async_list import AsyncList
|
from .async_list import AsyncList
|
||||||
from .callback_answer import CallbackAnswer
|
from .callback_answer import CallbackAnswer
|
||||||
from .chat import (
|
from .chat import (
|
||||||
|
@ -14,10 +15,23 @@ from .chat import (
|
||||||
from .chat_restriction import ChatRestriction
|
from .chat_restriction import ChatRestriction
|
||||||
from .dialog import Dialog
|
from .dialog import Dialog
|
||||||
from .draft import Draft
|
from .draft import Draft
|
||||||
from .file import File, InFileLike, OutFileLike, OutWrapper, expand_stripped_size
|
from .file import (
|
||||||
|
File,
|
||||||
|
InFileLike,
|
||||||
|
OutFileLike,
|
||||||
|
OutWrapper,
|
||||||
|
expand_stripped_size,
|
||||||
|
try_get_url_path,
|
||||||
|
)
|
||||||
from .inline_result import InlineResult
|
from .inline_result import InlineResult
|
||||||
from .login_token import LoginToken
|
from .login_token import LoginToken
|
||||||
from .message import Message, adapt_date, build_msg_map, generate_random_id
|
from .message import (
|
||||||
|
Message,
|
||||||
|
adapt_date,
|
||||||
|
build_msg_map,
|
||||||
|
generate_random_id,
|
||||||
|
parse_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
|
||||||
|
@ -25,6 +39,7 @@ from .recent_action import RecentAction
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AdminRight",
|
"AdminRight",
|
||||||
|
"AlbumBuilder",
|
||||||
"AsyncList",
|
"AsyncList",
|
||||||
"ChatRestriction",
|
"ChatRestriction",
|
||||||
"CallbackAnswer",
|
"CallbackAnswer",
|
||||||
|
@ -43,12 +58,14 @@ __all__ = [
|
||||||
"OutFileLike",
|
"OutFileLike",
|
||||||
"OutWrapper",
|
"OutWrapper",
|
||||||
"expand_stripped_size",
|
"expand_stripped_size",
|
||||||
|
"try_get_url_path",
|
||||||
"InlineResult",
|
"InlineResult",
|
||||||
"LoginToken",
|
"LoginToken",
|
||||||
"Message",
|
"Message",
|
||||||
"adapt_date",
|
"adapt_date",
|
||||||
"build_msg_map",
|
"build_msg_map",
|
||||||
"generate_random_id",
|
"generate_random_id",
|
||||||
|
"parse_message",
|
||||||
"NoPublicConstructor",
|
"NoPublicConstructor",
|
||||||
"Participant",
|
"Participant",
|
||||||
"PasswordToken",
|
"PasswordToken",
|
||||||
|
|
239
client/src/telethon/_impl/client/types/album_builder.py
Normal file
239
client/src/telethon/_impl/client/types/album_builder.py
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import mimetypes
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, List, Optional, Union
|
||||||
|
|
||||||
|
from ...tl import abcs, functions, types
|
||||||
|
from .chat import ChatLike
|
||||||
|
from .file import InFileLike, try_get_url_path
|
||||||
|
from .message import Message, generate_random_id, parse_message
|
||||||
|
from .meta import NoPublicConstructor
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..client.client import Client
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumBuilder(metaclass=NoPublicConstructor):
|
||||||
|
"""
|
||||||
|
Album builder to prepare albums with multiple files before sending it all at once.
|
||||||
|
|
||||||
|
This class is constructed by calling :meth:`telethon.Client.prepare_album`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *, client: Client):
|
||||||
|
self._client = client
|
||||||
|
self._medias: List[types.InputSingleMedia] = []
|
||||||
|
|
||||||
|
async def add_photo(
|
||||||
|
self,
|
||||||
|
file: Union[str, Path, InFileLike],
|
||||||
|
*,
|
||||||
|
size: Optional[int] = None,
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
caption_markdown: Optional[str] = None,
|
||||||
|
caption_html: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Add a photo to the album.
|
||||||
|
|
||||||
|
:param file:
|
||||||
|
The photo to attach to the album.
|
||||||
|
|
||||||
|
This behaves the same way as the file parameter in :meth:`telethon.Client.send_file`,
|
||||||
|
*except* that it cannot be previously-sent media.
|
||||||
|
|
||||||
|
:param size: See :meth:`telethon.Client.send_file`.
|
||||||
|
:param caption: See :ref:`formatting`.
|
||||||
|
:param caption_markdown: See :ref:`formatting`.
|
||||||
|
:param caption_html: See :ref:`formatting`.
|
||||||
|
"""
|
||||||
|
input_media: abcs.InputMedia
|
||||||
|
if try_get_url_path(file) is not None:
|
||||||
|
assert isinstance(file, str)
|
||||||
|
input_media = types.InputMediaPhotoExternal(
|
||||||
|
spoiler=False, url=file, ttl_seconds=None
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
input_file, _ = await self._client._upload(file, size, "a.jpg")
|
||||||
|
input_media = types.InputMediaUploadedPhoto(
|
||||||
|
spoiler=False, file=input_file, stickers=None, ttl_seconds=None
|
||||||
|
)
|
||||||
|
|
||||||
|
media = await self._client(
|
||||||
|
functions.messages.upload_media(
|
||||||
|
peer=types.InputPeerSelf(), media=input_media
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert isinstance(media, types.MessageMediaPhoto)
|
||||||
|
assert isinstance(media.photo, types.Photo)
|
||||||
|
input_media = types.InputMediaPhoto(
|
||||||
|
spoiler=media.spoiler,
|
||||||
|
id=types.InputPhoto(
|
||||||
|
id=media.photo.id,
|
||||||
|
access_hash=media.photo.access_hash,
|
||||||
|
file_reference=media.photo.file_reference,
|
||||||
|
),
|
||||||
|
ttl_seconds=media.ttl_seconds,
|
||||||
|
)
|
||||||
|
message, entities = parse_message(
|
||||||
|
text=caption, markdown=caption_markdown, html=caption_html, allow_empty=True
|
||||||
|
)
|
||||||
|
self._medias.append(
|
||||||
|
types.InputSingleMedia(
|
||||||
|
media=input_media,
|
||||||
|
random_id=generate_random_id(),
|
||||||
|
message=message,
|
||||||
|
entities=entities,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def add_video(
|
||||||
|
self,
|
||||||
|
file: Union[str, Path, InFileLike],
|
||||||
|
*,
|
||||||
|
size: Optional[int] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
mime_type: Optional[str] = None,
|
||||||
|
duration: Optional[float] = None,
|
||||||
|
width: Optional[int] = None,
|
||||||
|
height: Optional[int] = None,
|
||||||
|
round: bool = False,
|
||||||
|
supports_streaming: bool = False,
|
||||||
|
muted: bool = False,
|
||||||
|
caption: Optional[str] = None,
|
||||||
|
caption_markdown: Optional[str] = None,
|
||||||
|
caption_html: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Add a video to the album.
|
||||||
|
|
||||||
|
:param file:
|
||||||
|
The video to attach to the album.
|
||||||
|
|
||||||
|
This behaves the same way as the file parameter in :meth:`telethon.Client.send_file`,
|
||||||
|
*except* that it cannot be previously-sent media.
|
||||||
|
|
||||||
|
:param size: See :meth:`telethon.Client.send_file`.
|
||||||
|
:param name: See :meth:`telethon.Client.send_file`.
|
||||||
|
:param mime_type: See :meth:`telethon.Client.send_file`.
|
||||||
|
:param duration: See :meth:`telethon.Client.send_file`.
|
||||||
|
:param width: See :meth:`telethon.Client.send_file`.
|
||||||
|
:param height: See :meth:`telethon.Client.send_file`.
|
||||||
|
:param round: See :meth:`telethon.Client.send_file`.
|
||||||
|
:param supports_streaming: See :meth:`telethon.Client.send_file`.
|
||||||
|
:param muted: See :meth:`telethon.Client.send_file`.
|
||||||
|
:param caption: See :ref:`formatting`.
|
||||||
|
:param caption_markdown: See :ref:`formatting`.
|
||||||
|
:param caption_html: See :ref:`formatting`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
input_media: abcs.InputMedia
|
||||||
|
if try_get_url_path(file) is not None:
|
||||||
|
assert isinstance(file, str)
|
||||||
|
input_media = types.InputMediaDocumentExternal(
|
||||||
|
spoiler=False, url=file, ttl_seconds=None
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
input_file, name = await self._client._upload(file, size, name)
|
||||||
|
if mime_type is None:
|
||||||
|
mime_type, _ = mimetypes.guess_type(name, strict=False)
|
||||||
|
if mime_type is None:
|
||||||
|
mime_type = "application/octet-stream"
|
||||||
|
|
||||||
|
attributes: List[abcs.DocumentAttribute] = []
|
||||||
|
attributes.append(types.DocumentAttributeFilename(file_name=name))
|
||||||
|
if duration is not None and width is not None and height is not None:
|
||||||
|
attributes.append(
|
||||||
|
types.DocumentAttributeVideo(
|
||||||
|
round_message=round,
|
||||||
|
supports_streaming=supports_streaming,
|
||||||
|
nosound=muted,
|
||||||
|
duration=duration,
|
||||||
|
w=width,
|
||||||
|
h=height,
|
||||||
|
preload_prefix_size=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
input_media = types.InputMediaUploadedDocument(
|
||||||
|
nosound_video=muted,
|
||||||
|
force_file=False,
|
||||||
|
spoiler=False,
|
||||||
|
file=input_file,
|
||||||
|
thumb=None,
|
||||||
|
mime_type=mime_type,
|
||||||
|
attributes=attributes,
|
||||||
|
stickers=None,
|
||||||
|
ttl_seconds=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
media = await self._client(
|
||||||
|
functions.messages.upload_media(
|
||||||
|
peer=types.InputPeerEmpty(), media=input_media
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert isinstance(media, types.MessageMediaDocument)
|
||||||
|
assert isinstance(media.document, types.Document)
|
||||||
|
input_media = types.InputMediaDocument(
|
||||||
|
spoiler=media.spoiler,
|
||||||
|
id=types.InputDocument(
|
||||||
|
id=media.document.id,
|
||||||
|
access_hash=media.document.access_hash,
|
||||||
|
file_reference=media.document.file_reference,
|
||||||
|
),
|
||||||
|
ttl_seconds=media.ttl_seconds,
|
||||||
|
query=None,
|
||||||
|
)
|
||||||
|
message, entities = parse_message(
|
||||||
|
text=caption, markdown=caption_markdown, html=caption_html, allow_empty=True
|
||||||
|
)
|
||||||
|
self._medias.append(
|
||||||
|
types.InputSingleMedia(
|
||||||
|
media=input_media,
|
||||||
|
random_id=generate_random_id(),
|
||||||
|
message=message,
|
||||||
|
entities=entities,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send(
|
||||||
|
self, chat: ChatLike, *, reply_to: Optional[int] = None
|
||||||
|
) -> List[Message]:
|
||||||
|
"""
|
||||||
|
Send the album.
|
||||||
|
|
||||||
|
:return: All sent messages that are part of the album.
|
||||||
|
|
||||||
|
.. rubric:: Example
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
album = await client.prepare_album()
|
||||||
|
for photo in ('a.jpg', 'b.png'):
|
||||||
|
await album.add_photo(photo)
|
||||||
|
|
||||||
|
messages = await album.send(chat)
|
||||||
|
"""
|
||||||
|
peer = (await self._client._resolve_to_packed(chat))._to_input_peer()
|
||||||
|
msg_map = self._client._build_message_map(
|
||||||
|
await self._client(
|
||||||
|
functions.messages.send_multi_media(
|
||||||
|
silent=False,
|
||||||
|
background=False,
|
||||||
|
clear_draft=False,
|
||||||
|
noforwards=False,
|
||||||
|
update_stickersets_order=False,
|
||||||
|
peer=peer,
|
||||||
|
reply_to=types.InputReplyToMessage(
|
||||||
|
reply_to_msg_id=reply_to, top_msg_id=None
|
||||||
|
)
|
||||||
|
if reply_to
|
||||||
|
else None,
|
||||||
|
multi_media=self._medias,
|
||||||
|
schedule_date=None,
|
||||||
|
send_as=None,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
peer,
|
||||||
|
)
|
||||||
|
return [msg_map.with_random_id(media.random_id) for media in self._medias]
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import weakref
|
import weakref
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, List, Optional, Union
|
||||||
|
|
||||||
from ....tl import abcs, types
|
from ....tl import abcs, types
|
||||||
from .button import Button
|
from .button import Button
|
||||||
|
@ -23,6 +23,40 @@ def as_concrete_row(row: abcs.KeyboardButtonRow) -> types.KeyboardButtonRow:
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def build_keyboard(
|
||||||
|
btns: Optional[Union[List[Button], List[List[Button]]]]
|
||||||
|
) -> Optional[abcs.ReplyMarkup]:
|
||||||
|
# list[button] -> list[list[button]]
|
||||||
|
# This does allow for "invalid" inputs (mixing lists and non-lists), but that's acceptable.
|
||||||
|
buttons_lists_iter = (
|
||||||
|
button if isinstance(button, list) else [button] for button in (btns or [])
|
||||||
|
)
|
||||||
|
# Remove empty rows (also making it easy to check if all-empty).
|
||||||
|
buttons_lists = [bs for bs in buttons_lists_iter if bs]
|
||||||
|
|
||||||
|
if not buttons_lists:
|
||||||
|
return None
|
||||||
|
|
||||||
|
rows: List[abcs.KeyboardButtonRow] = [
|
||||||
|
types.KeyboardButtonRow(buttons=[btn._raw for btn in btns])
|
||||||
|
for btns in buttons_lists
|
||||||
|
]
|
||||||
|
|
||||||
|
# Guaranteed to have at least one, first one used to check if it's inline.
|
||||||
|
# If the user mixed inline with non-inline, Telegram will complain.
|
||||||
|
if isinstance(buttons_lists[0][0], InlineButton):
|
||||||
|
return types.ReplyInlineMarkup(rows=rows)
|
||||||
|
else:
|
||||||
|
return types.ReplyKeyboardMarkup(
|
||||||
|
resize=False,
|
||||||
|
single_use=False,
|
||||||
|
selective=False,
|
||||||
|
persistent=False,
|
||||||
|
rows=rows,
|
||||||
|
placeholder=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_button(message: Message, raw: abcs.KeyboardButton) -> Button:
|
def create_button(message: Message, raw: abcs.KeyboardButton) -> Button:
|
||||||
"""
|
"""
|
||||||
Create a custom button from a Telegram button.
|
Create a custom button from a Telegram button.
|
||||||
|
|
|
@ -163,7 +163,7 @@ class Draft(metaclass=NoPublicConstructor):
|
||||||
if chat := self._chat_map.get(peer_id(self._peer)):
|
if chat := self._chat_map.get(peer_id(self._peer)):
|
||||||
packed = chat.pack()
|
packed = chat.pack()
|
||||||
if packed is None:
|
if packed is None:
|
||||||
packed = await self._client.resolve_to_packed(peer_id(self._peer))
|
packed = await self._client._resolve_to_packed(peer_id(self._peer))
|
||||||
return packed
|
return packed
|
||||||
|
|
||||||
async def send(self) -> Message:
|
async def send(self) -> Message:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
import urllib.parse
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
from io import BufferedWriter
|
from io import BufferedWriter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -68,6 +69,15 @@ def photo_size_dimensions(
|
||||||
raise RuntimeError("unexpected case")
|
raise RuntimeError("unexpected case")
|
||||||
|
|
||||||
|
|
||||||
|
def try_get_url_path(maybe_url: Union[str, Path, InFileLike]) -> Optional[str]:
|
||||||
|
if not isinstance(maybe_url, str):
|
||||||
|
return None
|
||||||
|
lowercase = maybe_url.lower()
|
||||||
|
if lowercase.startswith("http://") or lowercase.startswith("https://"):
|
||||||
|
return urllib.parse.urlparse(maybe_url).path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class InFileLike(Protocol):
|
class InFileLike(Protocol):
|
||||||
"""
|
"""
|
||||||
A :term:`file-like object` used for input only.
|
A :term:`file-like object` used for input only.
|
||||||
|
|
|
@ -2,10 +2,15 @@ from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Self, Union
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Self, Tuple, Union
|
||||||
|
|
||||||
from ...tl import abcs, types
|
from ...tl import abcs, types
|
||||||
from ..parsers import generate_html_message, generate_markdown_message
|
from ..parsers import (
|
||||||
|
generate_html_message,
|
||||||
|
generate_markdown_message,
|
||||||
|
parse_html_message,
|
||||||
|
parse_markdown_message,
|
||||||
|
)
|
||||||
from .buttons import Button, as_concrete_row, create_button
|
from .buttons import Button, as_concrete_row, create_button
|
||||||
from .chat import Chat, ChatLike, expand_peer, peer_id
|
from .chat import Chat, ChatLike, expand_peer, peer_id
|
||||||
from .file import File
|
from .file import File
|
||||||
|
@ -276,6 +281,26 @@ class Message(metaclass=NoPublicConstructor):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def incoming(self) -> bool:
|
||||||
|
"""
|
||||||
|
:data:`True` if the message is incoming.
|
||||||
|
This would mean another user sent it, and the currently logged-in user received it.
|
||||||
|
|
||||||
|
This is usually the opposite of :attr:`outgoing`, although some messages can be neither.
|
||||||
|
"""
|
||||||
|
return getattr(self._raw, "out", None) is False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def outgoing(self) -> bool:
|
||||||
|
"""
|
||||||
|
:data:`True` if the message is outgoing.
|
||||||
|
This would mean the currently logged-in user sent it.
|
||||||
|
|
||||||
|
This is usually the opposite of :attr:`incoming`, although some messages can be neither.
|
||||||
|
"""
|
||||||
|
return getattr(self._raw, "out", None) is True
|
||||||
|
|
||||||
async def get_replied_message(self) -> Optional[Message]:
|
async def get_replied_message(self) -> Optional[Message]:
|
||||||
"""
|
"""
|
||||||
Alias for :meth:`telethon.Client.get_messages_with_ids`.
|
Alias for :meth:`telethon.Client.get_messages_with_ids`.
|
||||||
|
@ -465,3 +490,28 @@ def build_msg_map(
|
||||||
msg.id: msg
|
msg.id: msg
|
||||||
for msg in (Message._from_raw(client, m, chat_map) for m in messages)
|
for msg in (Message._from_raw(client, m, chat_map) for m in messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_message(
|
||||||
|
*,
|
||||||
|
text: Optional[str],
|
||||||
|
markdown: Optional[str],
|
||||||
|
html: Optional[str],
|
||||||
|
allow_empty: bool,
|
||||||
|
) -> Tuple[str, Optional[List[abcs.MessageEntity]]]:
|
||||||
|
cnt = sum((text is not None, markdown is not None, html is not None))
|
||||||
|
if cnt != 1:
|
||||||
|
if cnt == 0 and allow_empty:
|
||||||
|
return "", None
|
||||||
|
raise ValueError("must specify exactly one of text, markdown or html")
|
||||||
|
|
||||||
|
if text is not None:
|
||||||
|
parsed, entities = text, None
|
||||||
|
elif markdown is not None:
|
||||||
|
parsed, entities = parse_markdown_message(markdown)
|
||||||
|
elif html is not None:
|
||||||
|
parsed, entities = parse_html_message(html)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("unexpected case")
|
||||||
|
|
||||||
|
return parsed, entities or None
|
||||||
|
|
|
@ -45,6 +45,11 @@ from ...tl.types import (
|
||||||
UpdateShortSentMessage,
|
UpdateShortSentMessage,
|
||||||
UpdatesTooLong,
|
UpdatesTooLong,
|
||||||
)
|
)
|
||||||
|
from ...tl.types.messages import (
|
||||||
|
AffectedFoundMessages,
|
||||||
|
AffectedHistory,
|
||||||
|
AffectedMessages,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
CONTAINER_MAX_LENGTH,
|
CONTAINER_MAX_LENGTH,
|
||||||
CONTAINER_MAX_SIZE,
|
CONTAINER_MAX_SIZE,
|
||||||
|
@ -69,6 +74,9 @@ UPDATE_IDS = {
|
||||||
UpdateShortMessage.constructor_id(),
|
UpdateShortMessage.constructor_id(),
|
||||||
UpdateShortSentMessage.constructor_id(),
|
UpdateShortSentMessage.constructor_id(),
|
||||||
UpdatesTooLong.constructor_id(),
|
UpdatesTooLong.constructor_id(),
|
||||||
|
AffectedFoundMessages.constructor_id(),
|
||||||
|
AffectedHistory.constructor_id(),
|
||||||
|
AffectedMessages.constructor_id(),
|
||||||
}
|
}
|
||||||
|
|
||||||
HEADER_LEN = 8 + 8 # salt, client_id
|
HEADER_LEN = 8 + 8 # salt, client_id
|
||||||
|
|
|
@ -5,7 +5,7 @@ import time
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from asyncio import FIRST_COMPLETED, Event, Future
|
from asyncio import FIRST_COMPLETED, Event, Future
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Generic, List, Optional, Protocol, Self, Tuple, TypeVar
|
from typing import Generic, List, Optional, Protocol, Self, Tuple, Type, TypeVar
|
||||||
|
|
||||||
from ..crypto import AuthKey
|
from ..crypto import AuthKey
|
||||||
from ..mtproto import (
|
from ..mtproto import (
|
||||||
|
@ -21,7 +21,10 @@ from ..mtproto import (
|
||||||
)
|
)
|
||||||
from ..tl import Request as RemoteCall
|
from ..tl import Request as RemoteCall
|
||||||
from ..tl.abcs import Updates
|
from ..tl.abcs import Updates
|
||||||
|
from ..tl.core import Serializable
|
||||||
from ..tl.mtproto.functions import ping_delay_disconnect
|
from ..tl.mtproto.functions import ping_delay_disconnect
|
||||||
|
from ..tl.types import UpdateDeleteMessages, UpdateShort
|
||||||
|
from ..tl.types.messages import AffectedFoundMessages, AffectedHistory, AffectedMessages
|
||||||
|
|
||||||
MAXIMUM_DATA = (1024 * 1024) + (8 * 1024)
|
MAXIMUM_DATA = (1024 * 1024) + (8 * 1024)
|
||||||
|
|
||||||
|
@ -315,12 +318,41 @@ class Sender:
|
||||||
try:
|
try:
|
||||||
u = Updates.from_bytes(update)
|
u = Updates.from_bytes(update)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self._logger.warning(
|
cid = struct.unpack_from("I", update)[0]
|
||||||
"failed to deserialize incoming update; make sure the session is not in use elsewhere: %s",
|
alt_classes: Tuple[Type[Serializable], ...] = (
|
||||||
update.hex(),
|
AffectedFoundMessages,
|
||||||
|
AffectedHistory,
|
||||||
|
AffectedMessages,
|
||||||
)
|
)
|
||||||
else:
|
for cls in alt_classes:
|
||||||
updates.append(u)
|
if cid == cls.constructor_id():
|
||||||
|
affected = cls.from_bytes(update)
|
||||||
|
# mypy struggles with the types here quite a bit
|
||||||
|
assert isinstance(
|
||||||
|
affected,
|
||||||
|
(
|
||||||
|
AffectedFoundMessages,
|
||||||
|
AffectedHistory,
|
||||||
|
AffectedMessages,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
u = UpdateShort(
|
||||||
|
update=UpdateDeleteMessages(
|
||||||
|
messages=[],
|
||||||
|
pts=affected.pts,
|
||||||
|
pts_count=affected.pts_count,
|
||||||
|
),
|
||||||
|
date=0,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self._logger.warning(
|
||||||
|
"failed to deserialize incoming update; make sure the session is not in use elsewhere: %s",
|
||||||
|
update.hex(),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
updates.append(u)
|
||||||
|
|
||||||
for msg_id, ret in result.rpc_results:
|
for msg_id, ret in result.rpc_results:
|
||||||
for i, req in enumerate(self._requests):
|
for i, req in enumerate(self._requests):
|
||||||
|
|
|
@ -61,6 +61,7 @@ class Gap(ValueError):
|
||||||
return "Gap()"
|
return "Gap()"
|
||||||
|
|
||||||
|
|
||||||
|
NO_DATE = 0 # used on adapted messages.affected* from lower layers
|
||||||
NO_SEQ = 0
|
NO_SEQ = 0
|
||||||
|
|
||||||
NO_PTS = 0
|
NO_PTS = 0
|
||||||
|
|
|
@ -13,6 +13,7 @@ from .defs import (
|
||||||
ENTRY_ACCOUNT,
|
ENTRY_ACCOUNT,
|
||||||
ENTRY_SECRET,
|
ENTRY_SECRET,
|
||||||
LOG_LEVEL_TRACE,
|
LOG_LEVEL_TRACE,
|
||||||
|
NO_DATE,
|
||||||
NO_PTS,
|
NO_PTS,
|
||||||
NO_SEQ,
|
NO_SEQ,
|
||||||
NO_UPDATES_TIMEOUT,
|
NO_UPDATES_TIMEOUT,
|
||||||
|
@ -294,9 +295,10 @@ class MessageBox:
|
||||||
if any_pts_applied:
|
if any_pts_applied:
|
||||||
if __debug__:
|
if __debug__:
|
||||||
self._trace("updating seq as local pts was updated too")
|
self._trace("updating seq as local pts was updated too")
|
||||||
self.date = datetime.datetime.fromtimestamp(
|
if combined.date != NO_DATE:
|
||||||
combined.date, tz=datetime.timezone.utc
|
self.date = datetime.datetime.fromtimestamp(
|
||||||
)
|
combined.date, tz=datetime.timezone.utc
|
||||||
|
)
|
||||||
if combined.seq != NO_SEQ:
|
if combined.seq != NO_SEQ:
|
||||||
self.seq = combined.seq
|
self.seq = combined.seq
|
||||||
|
|
||||||
|
|
|
@ -82,9 +82,8 @@ class Reader:
|
||||||
assert self._pos <= self._len
|
assert self._pos <= self._len
|
||||||
cid = struct.unpack("<I", self._view[self._pos - 4 : self._pos])[0]
|
cid = struct.unpack("<I", self._view[self._pos - 4 : self._pos])[0]
|
||||||
ty = self._get_ty(cid)
|
ty = self._get_ty(cid)
|
||||||
if ty is None:
|
if ty is None or not issubclass(ty, cls):
|
||||||
raise ValueError(f"No type found for constructor ID: {cid:x}")
|
raise ValueError(f"No type found for constructor ID of {cls}: {cid:x}")
|
||||||
assert issubclass(ty, cls)
|
|
||||||
return ty._read_from(self)
|
return ty._read_from(self)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ from .._impl.client.events.filters import (
|
||||||
All,
|
All,
|
||||||
Any,
|
Any,
|
||||||
Chats,
|
Chats,
|
||||||
|
ChatType,
|
||||||
Command,
|
Command,
|
||||||
Filter,
|
Filter,
|
||||||
Forward,
|
Forward,
|
||||||
|
@ -21,13 +22,13 @@ from .._impl.client.events.filters import (
|
||||||
Reply,
|
Reply,
|
||||||
Senders,
|
Senders,
|
||||||
Text,
|
Text,
|
||||||
TextOnly,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"All",
|
"All",
|
||||||
"Any",
|
"Any",
|
||||||
"Chats",
|
"Chats",
|
||||||
|
"ChatType",
|
||||||
"Command",
|
"Command",
|
||||||
"Filter",
|
"Filter",
|
||||||
"Forward",
|
"Forward",
|
||||||
|
@ -38,5 +39,4 @@ __all__ = [
|
||||||
"Reply",
|
"Reply",
|
||||||
"Senders",
|
"Senders",
|
||||||
"Text",
|
"Text",
|
||||||
"TextOnly",
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -3,6 +3,7 @@ Classes for the various objects the library returns.
|
||||||
"""
|
"""
|
||||||
from .._impl.client.types import (
|
from .._impl.client.types import (
|
||||||
AdminRight,
|
AdminRight,
|
||||||
|
AlbumBuilder,
|
||||||
AsyncList,
|
AsyncList,
|
||||||
CallbackAnswer,
|
CallbackAnswer,
|
||||||
Channel,
|
Channel,
|
||||||
|
@ -25,6 +26,7 @@ from .._impl.session import PackedChat, PackedType
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AdminRight",
|
"AdminRight",
|
||||||
|
"AlbumBuilder",
|
||||||
"AsyncList",
|
"AsyncList",
|
||||||
"CallbackAnswer",
|
"CallbackAnswer",
|
||||||
"Channel",
|
"Channel",
|
||||||
|
|
|
@ -94,7 +94,7 @@ def generate(fs: FakeFs, tl: ParsedTl) -> None:
|
||||||
|
|
||||||
if type_path not in fs:
|
if type_path not in fs:
|
||||||
writer.write("import struct")
|
writer.write("import struct")
|
||||||
writer.write("from typing import List, Optional, Self")
|
writer.write("from typing import List, Optional, Self, Sequence")
|
||||||
writer.write("from .. import abcs")
|
writer.write("from .. import abcs")
|
||||||
writer.write("from ..core import Reader, Serializable, serialize_bytes_to")
|
writer.write("from ..core import Reader, Serializable, serialize_bytes_to")
|
||||||
|
|
||||||
|
@ -118,7 +118,8 @@ def generate(fs: FakeFs, tl: ParsedTl) -> None:
|
||||||
# def __init__()
|
# def __init__()
|
||||||
if property_params:
|
if property_params:
|
||||||
params = "".join(
|
params = "".join(
|
||||||
f", {p.name}: {param_type_fmt(p.ty)}" for p in property_params
|
f", {p.name}: {param_type_fmt(p.ty, immutable=False)}"
|
||||||
|
for p in property_params
|
||||||
)
|
)
|
||||||
writer.write(f" def __init__(_s, *{params}) -> None:")
|
writer.write(f" def __init__(_s, *{params}) -> None:")
|
||||||
for p in property_params:
|
for p in property_params:
|
||||||
|
@ -158,15 +159,20 @@ def generate(fs: FakeFs, tl: ParsedTl) -> None:
|
||||||
|
|
||||||
if function_path not in fs:
|
if function_path not in fs:
|
||||||
writer.write("import struct")
|
writer.write("import struct")
|
||||||
writer.write("from typing import List, Optional, Self")
|
writer.write("from typing import List, Optional, Self, Sequence")
|
||||||
writer.write("from .. import abcs")
|
writer.write("from .. import abcs")
|
||||||
writer.write("from ..core import Request, serialize_bytes_to")
|
writer.write("from ..core import Request, serialize_bytes_to")
|
||||||
|
|
||||||
# def name(params, ...)
|
# def name(params, ...)
|
||||||
required_params = [p for p in functiondef.params if not is_computed(p.ty)]
|
required_params = [p for p in functiondef.params if not is_computed(p.ty)]
|
||||||
params = "".join(f", {p.name}: {param_type_fmt(p.ty)}" for p in required_params)
|
params = "".join(
|
||||||
|
f", {p.name}: {param_type_fmt(p.ty, immutable=True)}"
|
||||||
|
for p in required_params
|
||||||
|
)
|
||||||
star = "*" if params else ""
|
star = "*" if params else ""
|
||||||
return_ty = param_type_fmt(NormalParameter(ty=functiondef.ty, flag=None))
|
return_ty = param_type_fmt(
|
||||||
|
NormalParameter(ty=functiondef.ty, flag=None), immutable=False
|
||||||
|
)
|
||||||
writer.write(
|
writer.write(
|
||||||
f"def {to_method_name(functiondef.name)}({star}{params}) -> Request[{return_ty}]:"
|
f"def {to_method_name(functiondef.name)}({star}{params}) -> Request[{return_ty}]:"
|
||||||
)
|
)
|
||||||
|
|
|
@ -86,7 +86,7 @@ def inner_type_fmt(ty: Type) -> str:
|
||||||
return f"abcs.{ns}{to_class_name(ty.name)}"
|
return f"abcs.{ns}{to_class_name(ty.name)}"
|
||||||
|
|
||||||
|
|
||||||
def param_type_fmt(ty: BaseParameter) -> str:
|
def param_type_fmt(ty: BaseParameter, *, immutable: bool) -> str:
|
||||||
if isinstance(ty, FlagsParameter):
|
if isinstance(ty, FlagsParameter):
|
||||||
return "int"
|
return "int"
|
||||||
elif not isinstance(ty, NormalParameter):
|
elif not isinstance(ty, NormalParameter):
|
||||||
|
@ -104,7 +104,10 @@ def param_type_fmt(ty: BaseParameter) -> str:
|
||||||
res = "bytes" if inner_ty.name == "Object" else inner_type_fmt(inner_ty)
|
res = "bytes" if inner_ty.name == "Object" else inner_type_fmt(inner_ty)
|
||||||
|
|
||||||
if ty.ty.generic_arg:
|
if ty.ty.generic_arg:
|
||||||
res = f"List[{res}]"
|
if immutable:
|
||||||
|
res = f"Sequence[{res}]"
|
||||||
|
else:
|
||||||
|
res = f"List[{res}]"
|
||||||
|
|
||||||
if ty.flag and ty.ty.name != "true":
|
if ty.flag and ty.ty.name != "true":
|
||||||
res = f"Optional[{res}]"
|
res = f"Optional[{res}]"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user