Merge branch 'LonamiWebs:v2' into v2

This commit is contained in:
apepenkov 2023-11-09 11:06:51 +03:00 committed by GitHub
commit 96dd15f796
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 868 additions and 293 deletions

View File

@ -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

View File

@ -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)

View File

@ -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**.

View File

@ -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")

View File

@ -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(

View File

@ -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,

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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",
] ]

View File

@ -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): ...

View File

@ -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)

View File

@ -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"

View File

@ -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

View File

@ -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",

View 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]

View File

@ -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.

View File

@ -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:

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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",
] ]

View File

@ -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",

View File

@ -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}]:"
) )

View File

@ -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}]"