mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-01-24 16:24:15 +03:00
Add client.prepare_album
This commit is contained in:
parent
ae44426a78
commit
6047c689ca
|
@ -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,
|
||||
|
@ -1085,6 +1088,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:
|
||||
|
@ -1310,6 +1341,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 +1352,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 +1367,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 +1386,7 @@ class Client:
|
|||
self,
|
||||
chat,
|
||||
file,
|
||||
mime_type,
|
||||
size=size,
|
||||
name=name,
|
||||
duration=duration,
|
||||
|
@ -1360,6 +1396,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 +1424,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 +1478,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 +1578,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 +1591,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 +1636,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 +1662,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 +1682,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 +1700,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 +1725,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 +1747,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 +2019,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()
|
||||
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(
|
||||
|
||||
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,
|
||||
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(
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
@ -465,3 +470,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
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue
Block a user