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
|
||||
tools:
|
||||
python: "3.11"
|
||||
apt_packages:
|
||||
- graphviz
|
||||
|
||||
sphinx:
|
||||
configuration: client/doc/conf.py
|
||||
|
|
|
@ -318,7 +318,7 @@ In Telethon:
|
|||
.. code-block:: python
|
||||
|
||||
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)
|
||||
|
||||
# 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!\
|
||||
""")
|
||||
|
||||
# Handle all other messages with only 'text'
|
||||
@bot.on(events.NewMessage, TextOnly())
|
||||
# Handle all other messages without media (negating the filter using ~)
|
||||
@bot.on(events.NewMessage, ~Media())
|
||||
async def echo_message(message: NewMessage):
|
||||
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
|
||||
`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>`_:
|
||||
|
||||
* ``strong`` and ``b`` for **bold**.
|
||||
|
|
|
@ -37,6 +37,7 @@ from ..events import Event
|
|||
from ..events.filters import Filter
|
||||
from ..types import (
|
||||
AdminRight,
|
||||
AlbumBuilder,
|
||||
AsyncList,
|
||||
Chat,
|
||||
ChatLike,
|
||||
|
@ -53,8 +54,8 @@ from ..types import (
|
|||
PasswordToken,
|
||||
RecentAction,
|
||||
User,
|
||||
buttons,
|
||||
)
|
||||
from ..types import buttons as btns
|
||||
from .auth import (
|
||||
bot_sign_in,
|
||||
check_password,
|
||||
|
@ -77,10 +78,12 @@ from .dialogs import delete_dialog, edit_draft, get_dialogs, get_drafts
|
|||
from .files import (
|
||||
download,
|
||||
get_file_bytes,
|
||||
prepare_album,
|
||||
send_audio,
|
||||
send_file,
|
||||
send_photo,
|
||||
send_video,
|
||||
upload,
|
||||
)
|
||||
from .messages import (
|
||||
MessageMap,
|
||||
|
@ -115,6 +118,7 @@ from .updates import (
|
|||
set_handler_filter,
|
||||
)
|
||||
from .users import (
|
||||
get_chats,
|
||||
get_contacts,
|
||||
get_me,
|
||||
input_to_peer,
|
||||
|
@ -680,6 +684,40 @@ class Client:
|
|||
"""
|
||||
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]:
|
||||
"""
|
||||
Get the users in your contact list.
|
||||
|
@ -1085,6 +1123,34 @@ class Client:
|
|||
"""
|
||||
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(
|
||||
self, chat: ChatLike, message_id: Union[int, Literal["all"]]
|
||||
) -> None:
|
||||
|
@ -1169,28 +1235,6 @@ class Client:
|
|||
"""
|
||||
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:
|
||||
"""
|
||||
Resolve a username into a :term:`chat`.
|
||||
|
@ -1310,6 +1354,7 @@ class Client:
|
|||
self,
|
||||
chat: ChatLike,
|
||||
file: Union[str, Path, InFileLike, File],
|
||||
mime_type: Optional[str] = None,
|
||||
*,
|
||||
size: Optional[int] = None,
|
||||
name: Optional[str] = None,
|
||||
|
@ -1320,6 +1365,8 @@ class Client:
|
|||
caption: Optional[str] = None,
|
||||
caption_markdown: 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:
|
||||
"""
|
||||
Send an audio file.
|
||||
|
@ -1333,6 +1380,7 @@ class Client:
|
|||
:param file: See :meth:`send_file`.
|
||||
:param size: See :meth:`send_file`.
|
||||
:param name: See :meth:`send_file`.
|
||||
:param mime_type: See :meth:`send_file`.
|
||||
:param duration: See :meth:`send_file`.
|
||||
:param voice: See :meth:`send_file`.
|
||||
:param title: See :meth:`send_file`.
|
||||
|
@ -1351,6 +1399,7 @@ class Client:
|
|||
self,
|
||||
chat,
|
||||
file,
|
||||
mime_type,
|
||||
size=size,
|
||||
name=name,
|
||||
duration=duration,
|
||||
|
@ -1360,6 +1409,8 @@ class Client:
|
|||
caption=caption,
|
||||
caption_markdown=caption_markdown,
|
||||
caption_html=caption_html,
|
||||
reply_to=reply_to,
|
||||
buttons=buttons,
|
||||
)
|
||||
|
||||
async def send_file(
|
||||
|
@ -1386,6 +1437,8 @@ class Client:
|
|||
caption: Optional[str] = None,
|
||||
caption_markdown: 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:
|
||||
"""
|
||||
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.
|
||||
|
||||
This parameter **must** be specified when sending a previously-opened or in-memory files.
|
||||
The library will not attempt to read any ``name`` attributes the object may have.
|
||||
When given a :term:`file-like object`, if it has a ``.name`` :class:`str` property, it will be used.
|
||||
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:
|
||||
Override for the default mime-type.
|
||||
|
@ -1536,6 +1591,8 @@ class Client:
|
|||
caption=caption,
|
||||
caption_markdown=caption_markdown,
|
||||
caption_html=caption_html,
|
||||
reply_to=reply_to,
|
||||
buttons=buttons,
|
||||
)
|
||||
|
||||
async def send_message(
|
||||
|
@ -1547,9 +1604,7 @@ class Client:
|
|||
html: Optional[str] = None,
|
||||
link_preview: bool = False,
|
||||
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:
|
||||
"""
|
||||
Send a message.
|
||||
|
@ -1594,12 +1649,15 @@ class Client:
|
|||
*,
|
||||
size: Optional[int] = None,
|
||||
name: Optional[str] = None,
|
||||
mime_type: Optional[str] = None,
|
||||
compress: bool = True,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
caption: Optional[str] = None,
|
||||
caption_markdown: Optional[str] = None,
|
||||
caption_html: Optional[str] = None,
|
||||
reply_to: Optional[int] = None,
|
||||
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||
) -> Message:
|
||||
"""
|
||||
Send a photo file.
|
||||
|
@ -1617,6 +1675,7 @@ class Client:
|
|||
:param file: See :meth:`send_file`.
|
||||
:param size: See :meth:`send_file`.
|
||||
:param name: See :meth:`send_file`.
|
||||
:param mime_type: See :meth:`send_file`.
|
||||
:param compress: See :meth:`send_file`.
|
||||
:param width: See :meth:`send_file`.
|
||||
:param height: See :meth:`send_file`.
|
||||
|
@ -1636,12 +1695,15 @@ class Client:
|
|||
file,
|
||||
size=size,
|
||||
name=name,
|
||||
mime_type=mime_type,
|
||||
compress=compress,
|
||||
width=width,
|
||||
height=height,
|
||||
caption=caption,
|
||||
caption_markdown=caption_markdown,
|
||||
caption_html=caption_html,
|
||||
reply_to=reply_to,
|
||||
buttons=buttons,
|
||||
)
|
||||
|
||||
async def send_video(
|
||||
|
@ -1651,14 +1713,18 @@ class Client:
|
|||
*,
|
||||
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,
|
||||
reply_to: Optional[int] = None,
|
||||
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||
) -> Message:
|
||||
"""
|
||||
Send a video file.
|
||||
|
@ -1672,6 +1738,7 @@ class Client:
|
|||
:param file: See :meth:`send_file`.
|
||||
:param size: See :meth:`send_file`.
|
||||
:param name: See :meth:`send_file`.
|
||||
:param mime_type: See :meth:`send_file`.
|
||||
:param duration: See :meth:`send_file`.
|
||||
:param width: See :meth:`send_file`.
|
||||
:param height: See :meth:`send_file`.
|
||||
|
@ -1693,14 +1760,18 @@ class Client:
|
|||
file,
|
||||
size=size,
|
||||
name=name,
|
||||
mime_type=mime_type,
|
||||
duration=duration,
|
||||
width=width,
|
||||
height=height,
|
||||
round=round,
|
||||
supports_streaming=supports_streaming,
|
||||
muted=muted,
|
||||
caption=caption,
|
||||
caption_markdown=caption_markdown,
|
||||
caption_html=caption_html,
|
||||
reply_to=reply_to,
|
||||
buttons=buttons,
|
||||
)
|
||||
|
||||
async def set_chat_default_restrictions(
|
||||
|
@ -1961,6 +2032,11 @@ class Client:
|
|||
def _input_to_peer(self, input: Optional[abcs.InputPeer]) -> Optional[abcs.Peer]:
|
||||
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:
|
||||
if not self._sender:
|
||||
raise ConnectionError("not connected")
|
||||
|
|
|
@ -4,8 +4,15 @@ import time
|
|||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from ...tl import functions, types
|
||||
from ..types import AsyncList, ChatLike, Dialog, Draft, build_chat_map, build_msg_map
|
||||
from .messages import parse_message
|
||||
from ..types import (
|
||||
AsyncList,
|
||||
ChatLike,
|
||||
Dialog,
|
||||
Draft,
|
||||
build_chat_map,
|
||||
build_msg_map,
|
||||
parse_message,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import Client
|
||||
|
@ -125,7 +132,6 @@ async def edit_draft(
|
|||
message, entities = parse_message(
|
||||
text=text, markdown=markdown, html=html, allow_empty=False
|
||||
)
|
||||
assert isinstance(message, str)
|
||||
|
||||
result = await self(
|
||||
functions.messages.save_draft(
|
||||
|
|
|
@ -2,13 +2,13 @@ from __future__ import annotations
|
|||
|
||||
import hashlib
|
||||
import mimetypes
|
||||
import urllib.parse
|
||||
from inspect import isawaitable
|
||||
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 ..types import (
|
||||
AlbumBuilder,
|
||||
AsyncList,
|
||||
ChatLike,
|
||||
File,
|
||||
|
@ -16,10 +16,14 @@ from ..types import (
|
|||
Message,
|
||||
OutFileLike,
|
||||
OutWrapper,
|
||||
)
|
||||
from ..types import buttons as btns
|
||||
from ..types import (
|
||||
expand_stripped_size,
|
||||
generate_random_id,
|
||||
parse_message,
|
||||
try_get_url_path,
|
||||
)
|
||||
from .messages import parse_message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import Client
|
||||
|
@ -34,6 +38,10 @@ BIG_FILE_SIZE = 10 * 1024 * 1024
|
|||
math_round = round
|
||||
|
||||
|
||||
def prepare_album(self: Client) -> AlbumBuilder:
|
||||
return AlbumBuilder._create(client=self)
|
||||
|
||||
|
||||
async def send_photo(
|
||||
self: Client,
|
||||
chat: ChatLike,
|
||||
|
@ -41,12 +49,15 @@ async def send_photo(
|
|||
*,
|
||||
size: Optional[int] = None,
|
||||
name: Optional[str] = None,
|
||||
mime_type: Optional[str] = None,
|
||||
compress: bool = True,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
caption: Optional[str] = None,
|
||||
caption_markdown: Optional[str] = None,
|
||||
caption_html: Optional[str] = None,
|
||||
reply_to: Optional[int] = None,
|
||||
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||
) -> Message:
|
||||
return await send_file(
|
||||
self,
|
||||
|
@ -54,12 +65,17 @@ async def send_photo(
|
|||
file,
|
||||
size=size,
|
||||
name=name,
|
||||
mime_type="image/jpeg" # specific mime doesn't matter, only that it's image
|
||||
if compress
|
||||
else mime_type,
|
||||
compress=compress,
|
||||
width=width,
|
||||
height=height,
|
||||
caption=caption,
|
||||
caption_markdown=caption_markdown,
|
||||
caption_html=caption_html,
|
||||
reply_to=reply_to,
|
||||
buttons=buttons,
|
||||
)
|
||||
|
||||
|
||||
|
@ -67,6 +83,7 @@ async def send_audio(
|
|||
self: Client,
|
||||
chat: ChatLike,
|
||||
file: Union[str, Path, InFileLike, File],
|
||||
mime_type: Optional[str] = None,
|
||||
*,
|
||||
size: Optional[int] = None,
|
||||
name: Optional[str] = None,
|
||||
|
@ -77,6 +94,8 @@ async def send_audio(
|
|||
caption: Optional[str] = None,
|
||||
caption_markdown: 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:
|
||||
return await send_file(
|
||||
self,
|
||||
|
@ -84,6 +103,7 @@ async def send_audio(
|
|||
file,
|
||||
size=size,
|
||||
name=name,
|
||||
mime_type=mime_type,
|
||||
duration=duration,
|
||||
voice=voice,
|
||||
title=title,
|
||||
|
@ -91,6 +111,8 @@ async def send_audio(
|
|||
caption=caption,
|
||||
caption_markdown=caption_markdown,
|
||||
caption_html=caption_html,
|
||||
reply_to=reply_to,
|
||||
buttons=buttons,
|
||||
)
|
||||
|
||||
|
||||
|
@ -101,41 +123,40 @@ async def send_video(
|
|||
*,
|
||||
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,
|
||||
reply_to: Optional[int] = None,
|
||||
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]] = None,
|
||||
) -> Message:
|
||||
return await send_file(
|
||||
self,
|
||||
chat,
|
||||
file,
|
||||
size=size,
|
||||
mime_type=mime_type,
|
||||
name=name,
|
||||
duration=duration,
|
||||
width=width,
|
||||
height=height,
|
||||
round=round,
|
||||
supports_streaming=supports_streaming,
|
||||
muted=muted,
|
||||
caption=caption,
|
||||
caption_markdown=caption_markdown,
|
||||
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(
|
||||
self: Client,
|
||||
chat: ChatLike,
|
||||
|
@ -160,15 +181,18 @@ async def send_file(
|
|||
caption: Optional[str] = None,
|
||||
caption_markdown: 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, entities = parse_message(
|
||||
text=caption, markdown=caption_markdown, html=caption_html, allow_empty=True
|
||||
)
|
||||
assert isinstance(message, str)
|
||||
|
||||
# Re-send existing 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.
|
||||
input_media: abcs.InputMedia
|
||||
|
@ -190,23 +214,11 @@ async def send_file(
|
|||
input_media = types.InputMediaDocumentExternal(
|
||||
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.
|
||||
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)
|
||||
input_file, name = await upload(self, file, size, name)
|
||||
|
||||
# Mime is mandatory for documents, but we also use it to determine whether to send as photo.
|
||||
if mime_type is None:
|
||||
|
@ -249,7 +261,7 @@ async def send_file(
|
|||
round_message=round,
|
||||
supports_streaming=supports_streaming,
|
||||
nosound=muted,
|
||||
duration=int(math_round(duration)),
|
||||
duration=duration,
|
||||
w=width,
|
||||
h=height,
|
||||
preload_prefix_size=None,
|
||||
|
@ -268,7 +280,9 @@ async def send_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
|
||||
)
|
||||
|
||||
|
||||
async def do_send_file(
|
||||
|
@ -277,6 +291,8 @@ async def do_send_file(
|
|||
input_media: abcs.InputMedia,
|
||||
message: str,
|
||||
entities: Optional[List[abcs.MessageEntity]],
|
||||
reply_to: Optional[int],
|
||||
buttons: Optional[Union[List[btns.Button], List[List[btns.Button]]]],
|
||||
) -> Message:
|
||||
peer = (await client._resolve_to_packed(chat))._to_input_peer()
|
||||
random_id = generate_random_id()
|
||||
|
@ -289,11 +305,15 @@ async def do_send_file(
|
|||
noforwards=False,
|
||||
update_stickersets_order=False,
|
||||
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,
|
||||
message=message,
|
||||
random_id=random_id,
|
||||
reply_markup=None,
|
||||
reply_markup=btns.build_keyboard(buttons),
|
||||
entities=entities,
|
||||
schedule_date=None,
|
||||
send_as=None,
|
||||
|
@ -304,6 +324,31 @@ async def do_send_file(
|
|||
|
||||
|
||||
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,
|
||||
fd: InFileLike,
|
||||
size: int,
|
||||
|
|
|
@ -2,85 +2,18 @@ from __future__ import annotations
|
|||
|
||||
import datetime
|
||||
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 ...tl import abcs, functions, types
|
||||
from ..parsers import parse_html_message, parse_markdown_message
|
||||
from ..types import (
|
||||
AsyncList,
|
||||
Chat,
|
||||
ChatLike,
|
||||
Message,
|
||||
build_chat_map,
|
||||
buttons,
|
||||
generate_random_id,
|
||||
peer_id,
|
||||
)
|
||||
from ..types import AsyncList, Chat, ChatLike, Message, build_chat_map
|
||||
from ..types import buttons as btns
|
||||
from ..types import generate_random_id, parse_message, peer_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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(
|
||||
self: Client,
|
||||
chat: ChatLike,
|
||||
|
@ -90,16 +23,39 @@ async def send_message(
|
|||
html: Optional[str] = None,
|
||||
link_preview: bool = False,
|
||||
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:
|
||||
packed = await self._resolve_to_packed(chat)
|
||||
peer = packed._to_input_peer()
|
||||
random_id = generate_random_id()
|
||||
|
||||
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
|
||||
)
|
||||
random_id = generate_random_id()
|
||||
result = await self(
|
||||
functions.messages.send_message(
|
||||
request = functions.messages.send_message(
|
||||
no_webpage=not link_preview,
|
||||
silent=False,
|
||||
background=False,
|
||||
|
@ -114,33 +70,13 @@ async def send_message(
|
|||
else None,
|
||||
message=message,
|
||||
random_id=random_id,
|
||||
reply_markup=build_keyboard(buttons),
|
||||
reply_markup=btns.build_keyboard(buttons),
|
||||
entities=entities,
|
||||
schedule_date=None,
|
||||
send_as=None,
|
||||
)
|
||||
if isinstance(message, str)
|
||||
else functions.messages.send_message(
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
result = await self(request)
|
||||
if isinstance(result, types.UpdateShortSentMessage):
|
||||
return Message._from_defaults(
|
||||
self,
|
||||
|
@ -161,7 +97,7 @@ async def send_message(
|
|||
if reply_to
|
||||
else None,
|
||||
date=result.date,
|
||||
message=message if isinstance(message, str) else (message.text or ""),
|
||||
message=message,
|
||||
media=result.media,
|
||||
entities=result.entities,
|
||||
ttl_period=result.ttl_period,
|
||||
|
@ -184,7 +120,6 @@ async def edit_message(
|
|||
message, entities = parse_message(
|
||||
text=text, markdown=markdown, html=html, allow_empty=False
|
||||
)
|
||||
assert isinstance(message, str)
|
||||
return self._build_message_map(
|
||||
await self(
|
||||
functions.messages.edit_message(
|
||||
|
|
|
@ -154,5 +154,5 @@ async def dispatch_next(client: Client) -> None:
|
|||
for handler, filter in handlers:
|
||||
if not filter or filter(event):
|
||||
ret = await handler(event)
|
||||
if ret is Continue or client._shortcircuit_handlers:
|
||||
if ret is not Continue or client._shortcircuit_handlers:
|
||||
return
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, List, Optional, Sequence
|
||||
|
||||
from ...mtproto import RpcError
|
||||
from ...session import PackedChat, PackedType
|
||||
|
@ -13,6 +13,7 @@ from ..types import (
|
|||
Group,
|
||||
User,
|
||||
build_chat_map,
|
||||
expand_peer,
|
||||
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):
|
||||
return chat
|
||||
|
||||
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:
|
||||
return packed
|
||||
|
||||
|
@ -96,11 +136,11 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
|
|||
if isinstance(chat, types.InputPeerEmpty):
|
||||
raise ValueError("Cannot resolve chat")
|
||||
elif isinstance(chat, types.InputPeerSelf):
|
||||
if not self._session.user:
|
||||
if not client._session.user:
|
||||
raise ValueError("Cannot resolve chat")
|
||||
return PackedChat(
|
||||
ty=PackedType.BOT if self._session.user.bot else PackedType.USER,
|
||||
id=self._chat_hashes.self_id,
|
||||
ty=PackedType.BOT if client._session.user.bot else PackedType.USER,
|
||||
id=client._chat_hashes.self_id,
|
||||
access_hash=0,
|
||||
)
|
||||
elif isinstance(chat, types.InputPeerChat):
|
||||
|
@ -130,9 +170,9 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
|
|||
|
||||
if isinstance(chat, str):
|
||||
if chat.startswith("+"):
|
||||
resolved = await resolve_phone(self, chat)
|
||||
resolved = await resolve_phone(client, chat)
|
||||
elif chat == "me":
|
||||
if me := self._session.user:
|
||||
if me := client._session.user:
|
||||
return PackedChat(
|
||||
ty=PackedType.BOT if me.bot else PackedType.USER,
|
||||
id=me.id,
|
||||
|
@ -141,13 +181,13 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
|
|||
else:
|
||||
resolved = None
|
||||
else:
|
||||
resolved = await resolve_username(self, username=chat)
|
||||
resolved = await resolve_username(client, username=chat)
|
||||
|
||||
if resolved and (packed := resolved.pack()) is not None:
|
||||
return packed
|
||||
|
||||
if isinstance(chat, int):
|
||||
packed = self._chat_hashes.get(chat)
|
||||
packed = client._chat_hashes.get(chat)
|
||||
if packed is None:
|
||||
raise ValueError("Cannot resolve chat")
|
||||
return packed
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from .combinators import All, Any, Filter, Not
|
||||
from .common import Chats, Senders
|
||||
from .messages import Command, Forward, Incoming, Media, Outgoing, Reply, Text, TextOnly
|
||||
from .common import Chats, ChatType, Senders
|
||||
from .messages import Command, Forward, Incoming, Media, Outgoing, Reply, Text
|
||||
|
||||
__all__ = [
|
||||
"All",
|
||||
|
@ -8,6 +8,7 @@ __all__ = [
|
|||
"Filter",
|
||||
"Not",
|
||||
"Chats",
|
||||
"ChatType",
|
||||
"Senders",
|
||||
"Command",
|
||||
"Forward",
|
||||
|
@ -16,5 +17,4 @@ __all__ = [
|
|||
"Outgoing",
|
||||
"Reply",
|
||||
"Text",
|
||||
"TextOnly",
|
||||
]
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import abc
|
||||
import typing
|
||||
from typing import Callable, Tuple
|
||||
from typing import Callable, Tuple, TypeAlias
|
||||
|
||||
from ..event import Event
|
||||
|
||||
Filter = Callable[[Event], bool]
|
||||
Filter: TypeAlias = Callable[[Event], bool]
|
||||
|
||||
|
||||
class Combinable(abc.ABC):
|
||||
|
@ -48,11 +48,12 @@ class Any(Combinable):
|
|||
"""
|
||||
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
|
||||
|
||||
from telethon.filters import Any, Command
|
||||
from telethon.events.filters import Any, Command
|
||||
|
||||
@bot.on(events.NewMessage, Any(Command('/start'), Command('/help')))
|
||||
async def handler(event): ...
|
||||
|
@ -87,11 +88,12 @@ class All(Combinable):
|
|||
"""
|
||||
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
|
||||
|
||||
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+')))
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
from telethon.filters import All, Command
|
||||
from telethon.events.filters import All, Command
|
||||
|
||||
@bot.on(events.NewMessage, Not(Command('/start'))
|
||||
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 ..event import Event
|
||||
|
@ -8,20 +8,21 @@ from .combinators import Combinable
|
|||
class Chats(Combinable):
|
||||
"""
|
||||
Filter by ``event.chat.id``, if the event has a chat.
|
||||
|
||||
:param chat_ids: The chat identifiers to filter on.
|
||||
"""
|
||||
|
||||
__slots__ = ("_chats",)
|
||||
|
||||
def __init__(self, chat_id: Union[int, Sequence[int]], *chat_ids: int) -> None:
|
||||
self._chats = {chat_id} if isinstance(chat_id, int) else set(chat_id)
|
||||
self._chats.update(chat_ids)
|
||||
def __init__(self, chat_ids: Sequence[int]) -> None:
|
||||
self._chats = set(chat_ids)
|
||||
|
||||
@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:
|
||||
chat = getattr(event, "chat", None)
|
||||
|
@ -32,20 +33,21 @@ class Chats(Combinable):
|
|||
class Senders(Combinable):
|
||||
"""
|
||||
Filter by ``event.sender.id``, if the event has a sender.
|
||||
|
||||
:param sender_ids: The sender identifiers to filter on.
|
||||
"""
|
||||
|
||||
__slots__ = ("_senders",)
|
||||
|
||||
def __init__(self, sender_id: Union[int, Sequence[int]], *sender_ids: int) -> None:
|
||||
self._senders = {sender_id} if isinstance(sender_id, int) else set(sender_id)
|
||||
self._senders.update(sender_ids)
|
||||
def __init__(self, sender_ids: Sequence[int]) -> None:
|
||||
self._senders = set(sender_ids)
|
||||
|
||||
@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:
|
||||
sender = getattr(event, "sender", None)
|
||||
|
@ -55,37 +57,38 @@ class Senders(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",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
type: Union[Literal["user"], Literal["group"], Literal["broadcast"]],
|
||||
type: Type[Union[User, Group, Channel]],
|
||||
) -> None:
|
||||
if type == "user":
|
||||
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}")
|
||||
self._type = type
|
||||
|
||||
@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.
|
||||
"""
|
||||
if self._type == User:
|
||||
return "user"
|
||||
elif self._type == Group:
|
||||
return "group"
|
||||
elif self._type == Channel:
|
||||
return "broadcast"
|
||||
else:
|
||||
raise RuntimeError("unexpected case")
|
||||
return self._type
|
||||
|
||||
def __call__(self, event: Event) -> bool:
|
||||
sender = getattr(event, "chat", None)
|
||||
|
|
|
@ -21,7 +21,9 @@ class Text(Combinable):
|
|||
you need to manually perform the check inside the handler instead.
|
||||
|
||||
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",)
|
||||
|
@ -43,6 +45,8 @@ class Command(Combinable):
|
|||
filter ``Command('/help')`` will match both ``"/help"`` and ``"/help@bot"``, but not
|
||||
``"/list"`` or ``"/help@other"``.
|
||||
|
||||
:param command: The command to match on.
|
||||
|
||||
.. note::
|
||||
|
||||
The leading forward-slash is not automatically added!
|
||||
|
@ -58,7 +62,7 @@ class Command(Combinable):
|
|||
__slots__ = ("_cmd", "_username")
|
||||
|
||||
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}")
|
||||
|
||||
self._cmd = command
|
||||
|
@ -87,11 +91,7 @@ class Command(Combinable):
|
|||
|
||||
class Incoming(Combinable):
|
||||
"""
|
||||
Filter by ``event.incoming``, that is, messages sent from others to the
|
||||
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.
|
||||
Filter by ``event.incoming``, that is, messages sent from others to the logged-in account.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
@ -102,11 +102,9 @@ class Incoming(Combinable):
|
|||
|
||||
class Outgoing(Combinable):
|
||||
"""
|
||||
Filter by ``event.outgoing``, that is, messages sent from others to the
|
||||
logged-in account.
|
||||
Filter by ``event.outgoing``, that is, messages sent from the 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.
|
||||
This is not a reliable way to check that the update was not produced by the logged-in account in broadcast channels.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
@ -117,32 +115,24 @@ class Outgoing(Combinable):
|
|||
|
||||
class Forward(Combinable):
|
||||
"""
|
||||
Filter by ``event.forward``.
|
||||
Filter by ``event.forward_info``, that is, messages that have been forwarded from elsewhere.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
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):
|
||||
"""
|
||||
Filter by ``event.reply``.
|
||||
Filter by ``event.replied_message_id``, that is, messages which are a reply to another message.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __call__(self, event: Event) -> bool:
|
||||
return getattr(event, "reply", 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.
|
||||
"""
|
||||
return getattr(event, "replied_message_id", None) is not None
|
||||
|
||||
|
||||
class Media(Combinable):
|
||||
|
@ -156,6 +146,10 @@ class Media(Combinable):
|
|||
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.
|
||||
|
||||
:param types:
|
||||
The media types to filter on.
|
||||
This is all of them if none are specified.
|
||||
"""
|
||||
|
||||
PHOTO = "photo"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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 ..types import Chat, Message
|
||||
from ..types import Chat, Message, expand_peer, peer_id
|
||||
from .event import Event
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -14,6 +14,8 @@ class NewMessage(Event, Message):
|
|||
"""
|
||||
Occurs when a new message is sent or received.
|
||||
|
||||
This event can be treated as the :class:`~telethon.types.Message` itself.
|
||||
|
||||
.. caution::
|
||||
|
||||
Messages sent with the :class:`~telethon.Client` are also caught,
|
||||
|
@ -39,6 +41,8 @@ class NewMessage(Event, Message):
|
|||
class MessageEdited(Event, Message):
|
||||
"""
|
||||
Occurs when a new message is sent or received.
|
||||
|
||||
This event can be treated as the :class:`~telethon.types.Message` itself.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
|
@ -80,32 +84,96 @@ class MessageDeleted(Event):
|
|||
else:
|
||||
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):
|
||||
"""
|
||||
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:
|
||||
self._peer = peer
|
||||
self._max_id = max_id
|
||||
self._out = out
|
||||
def __init__(
|
||||
self,
|
||||
client: Client,
|
||||
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
|
||||
def _try_from_update(
|
||||
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
|
||||
) -> Optional[Self]:
|
||||
if isinstance(update, types.UpdateReadHistoryInbox):
|
||||
return cls._create(update.peer, update.max_id, False)
|
||||
elif isinstance(update, types.UpdateReadHistoryOutbox):
|
||||
return cls._create(update.peer, update.max_id, True)
|
||||
elif isinstance(update, types.UpdateReadChannelInbox):
|
||||
return cls._create(
|
||||
types.PeerChannel(channel_id=update.channel_id), update.max_id, False
|
||||
)
|
||||
elif isinstance(update, types.UpdateReadChannelOutbox):
|
||||
return cls._create(
|
||||
types.PeerChannel(channel_id=update.channel_id), update.max_id, True
|
||||
)
|
||||
if isinstance(
|
||||
update,
|
||||
(
|
||||
types.UpdateReadHistoryInbox,
|
||||
types.UpdateReadHistoryOutbox,
|
||||
types.UpdateReadChannelInbox,
|
||||
types.UpdateReadChannelOutbox,
|
||||
),
|
||||
):
|
||||
return cls._create(client, update, chat_map)
|
||||
else:
|
||||
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 .album_builder import AlbumBuilder
|
||||
from .async_list import AsyncList
|
||||
from .callback_answer import CallbackAnswer
|
||||
from .chat import (
|
||||
|
@ -14,10 +15,23 @@ from .chat import (
|
|||
from .chat_restriction import ChatRestriction
|
||||
from .dialog import Dialog
|
||||
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 .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 .participant import Participant
|
||||
from .password_token import PasswordToken
|
||||
|
@ -25,6 +39,7 @@ from .recent_action import RecentAction
|
|||
|
||||
__all__ = [
|
||||
"AdminRight",
|
||||
"AlbumBuilder",
|
||||
"AsyncList",
|
||||
"ChatRestriction",
|
||||
"CallbackAnswer",
|
||||
|
@ -43,12 +58,14 @@ __all__ = [
|
|||
"OutFileLike",
|
||||
"OutWrapper",
|
||||
"expand_stripped_size",
|
||||
"try_get_url_path",
|
||||
"InlineResult",
|
||||
"LoginToken",
|
||||
"Message",
|
||||
"adapt_date",
|
||||
"build_msg_map",
|
||||
"generate_random_id",
|
||||
"parse_message",
|
||||
"NoPublicConstructor",
|
||||
"Participant",
|
||||
"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
|
||||
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, List, Optional, Union
|
||||
|
||||
from ....tl import abcs, types
|
||||
from .button import Button
|
||||
|
@ -23,6 +23,40 @@ def as_concrete_row(row: abcs.KeyboardButtonRow) -> types.KeyboardButtonRow:
|
|||
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:
|
||||
"""
|
||||
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)):
|
||||
packed = chat.pack()
|
||||
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
|
||||
|
||||
async def send(self) -> Message:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import mimetypes
|
||||
import urllib.parse
|
||||
from inspect import isawaitable
|
||||
from io import BufferedWriter
|
||||
from pathlib import Path
|
||||
|
@ -68,6 +69,15 @@ def photo_size_dimensions(
|
|||
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):
|
||||
"""
|
||||
A :term:`file-like object` used for input only.
|
||||
|
|
|
@ -2,10 +2,15 @@ from __future__ import annotations
|
|||
|
||||
import datetime
|
||||
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 ..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 .chat import Chat, ChatLike, expand_peer, peer_id
|
||||
from .file import File
|
||||
|
@ -276,6 +281,26 @@ class Message(metaclass=NoPublicConstructor):
|
|||
|
||||
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]:
|
||||
"""
|
||||
Alias for :meth:`telethon.Client.get_messages_with_ids`.
|
||||
|
@ -465,3 +490,28 @@ def build_msg_map(
|
|||
msg.id: msg
|
||||
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,
|
||||
UpdatesTooLong,
|
||||
)
|
||||
from ...tl.types.messages import (
|
||||
AffectedFoundMessages,
|
||||
AffectedHistory,
|
||||
AffectedMessages,
|
||||
)
|
||||
from ..utils import (
|
||||
CONTAINER_MAX_LENGTH,
|
||||
CONTAINER_MAX_SIZE,
|
||||
|
@ -69,6 +74,9 @@ UPDATE_IDS = {
|
|||
UpdateShortMessage.constructor_id(),
|
||||
UpdateShortSentMessage.constructor_id(),
|
||||
UpdatesTooLong.constructor_id(),
|
||||
AffectedFoundMessages.constructor_id(),
|
||||
AffectedHistory.constructor_id(),
|
||||
AffectedMessages.constructor_id(),
|
||||
}
|
||||
|
||||
HEADER_LEN = 8 + 8 # salt, client_id
|
||||
|
|
|
@ -5,7 +5,7 @@ import time
|
|||
from abc import ABC
|
||||
from asyncio import FIRST_COMPLETED, Event, Future
|
||||
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 ..mtproto import (
|
||||
|
@ -21,7 +21,10 @@ from ..mtproto import (
|
|||
)
|
||||
from ..tl import Request as RemoteCall
|
||||
from ..tl.abcs import Updates
|
||||
from ..tl.core import Serializable
|
||||
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)
|
||||
|
||||
|
@ -315,11 +318,40 @@ class Sender:
|
|||
try:
|
||||
u = Updates.from_bytes(update)
|
||||
except ValueError:
|
||||
cid = struct.unpack_from("I", update)[0]
|
||||
alt_classes: Tuple[Type[Serializable], ...] = (
|
||||
AffectedFoundMessages,
|
||||
AffectedHistory,
|
||||
AffectedMessages,
|
||||
)
|
||||
for cls in alt_classes:
|
||||
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(),
|
||||
)
|
||||
else:
|
||||
continue
|
||||
|
||||
updates.append(u)
|
||||
|
||||
for msg_id, ret in result.rpc_results:
|
||||
|
|
|
@ -61,6 +61,7 @@ class Gap(ValueError):
|
|||
return "Gap()"
|
||||
|
||||
|
||||
NO_DATE = 0 # used on adapted messages.affected* from lower layers
|
||||
NO_SEQ = 0
|
||||
|
||||
NO_PTS = 0
|
||||
|
|
|
@ -13,6 +13,7 @@ from .defs import (
|
|||
ENTRY_ACCOUNT,
|
||||
ENTRY_SECRET,
|
||||
LOG_LEVEL_TRACE,
|
||||
NO_DATE,
|
||||
NO_PTS,
|
||||
NO_SEQ,
|
||||
NO_UPDATES_TIMEOUT,
|
||||
|
@ -294,6 +295,7 @@ class MessageBox:
|
|||
if any_pts_applied:
|
||||
if __debug__:
|
||||
self._trace("updating seq as local pts was updated too")
|
||||
if combined.date != NO_DATE:
|
||||
self.date = datetime.datetime.fromtimestamp(
|
||||
combined.date, tz=datetime.timezone.utc
|
||||
)
|
||||
|
|
|
@ -82,9 +82,8 @@ class Reader:
|
|||
assert self._pos <= self._len
|
||||
cid = struct.unpack("<I", self._view[self._pos - 4 : self._pos])[0]
|
||||
ty = self._get_ty(cid)
|
||||
if ty is None:
|
||||
raise ValueError(f"No type found for constructor ID: {cid:x}")
|
||||
assert issubclass(ty, cls)
|
||||
if ty is None or not issubclass(ty, cls):
|
||||
raise ValueError(f"No type found for constructor ID of {cls}: {cid:x}")
|
||||
return ty._read_from(self)
|
||||
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ from .._impl.client.events.filters import (
|
|||
All,
|
||||
Any,
|
||||
Chats,
|
||||
ChatType,
|
||||
Command,
|
||||
Filter,
|
||||
Forward,
|
||||
|
@ -21,13 +22,13 @@ from .._impl.client.events.filters import (
|
|||
Reply,
|
||||
Senders,
|
||||
Text,
|
||||
TextOnly,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"All",
|
||||
"Any",
|
||||
"Chats",
|
||||
"ChatType",
|
||||
"Command",
|
||||
"Filter",
|
||||
"Forward",
|
||||
|
@ -38,5 +39,4 @@ __all__ = [
|
|||
"Reply",
|
||||
"Senders",
|
||||
"Text",
|
||||
"TextOnly",
|
||||
]
|
||||
|
|
|
@ -3,6 +3,7 @@ Classes for the various objects the library returns.
|
|||
"""
|
||||
from .._impl.client.types import (
|
||||
AdminRight,
|
||||
AlbumBuilder,
|
||||
AsyncList,
|
||||
CallbackAnswer,
|
||||
Channel,
|
||||
|
@ -25,6 +26,7 @@ from .._impl.session import PackedChat, PackedType
|
|||
|
||||
__all__ = [
|
||||
"AdminRight",
|
||||
"AlbumBuilder",
|
||||
"AsyncList",
|
||||
"CallbackAnswer",
|
||||
"Channel",
|
||||
|
|
|
@ -94,7 +94,7 @@ def generate(fs: FakeFs, tl: ParsedTl) -> None:
|
|||
|
||||
if type_path not in fs:
|
||||
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 ..core import Reader, Serializable, serialize_bytes_to")
|
||||
|
||||
|
@ -118,7 +118,8 @@ def generate(fs: FakeFs, tl: ParsedTl) -> None:
|
|||
# def __init__()
|
||||
if property_params:
|
||||
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:")
|
||||
for p in property_params:
|
||||
|
@ -158,15 +159,20 @@ def generate(fs: FakeFs, tl: ParsedTl) -> None:
|
|||
|
||||
if function_path not in fs:
|
||||
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 ..core import Request, serialize_bytes_to")
|
||||
|
||||
# def name(params, ...)
|
||||
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 ""
|
||||
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(
|
||||
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)}"
|
||||
|
||||
|
||||
def param_type_fmt(ty: BaseParameter) -> str:
|
||||
def param_type_fmt(ty: BaseParameter, *, immutable: bool) -> str:
|
||||
if isinstance(ty, FlagsParameter):
|
||||
return "int"
|
||||
elif not isinstance(ty, NormalParameter):
|
||||
|
@ -104,6 +104,9 @@ def param_type_fmt(ty: BaseParameter) -> str:
|
|||
res = "bytes" if inner_ty.name == "Object" else inner_type_fmt(inner_ty)
|
||||
|
||||
if ty.ty.generic_arg:
|
||||
if immutable:
|
||||
res = f"Sequence[{res}]"
|
||||
else:
|
||||
res = f"List[{res}]"
|
||||
|
||||
if ty.flag and ty.ty.name != "true":
|
||||
|
|
Loading…
Reference in New Issue
Block a user