Work towards dialogs and drafts

This commit is contained in:
Lonami Exo
2023-10-19 21:36:54 +02:00
parent 864d5cd444
commit b8b9836cf7
10 changed files with 538 additions and 88 deletions

View File

@@ -70,7 +70,7 @@ from .chats import (
set_banned_rights,
set_default_rights,
)
from .dialogs import delete_dialog, get_dialogs, get_drafts
from .dialogs import delete_dialog, edit_draft, get_dialogs, get_drafts
from .files import (
download,
get_file_bytes,
@@ -499,7 +499,7 @@ class Client:
text: Optional[str] = None,
markdown: Optional[str] = None,
html: Optional[str] = None,
link_preview: Optional[bool] = None,
link_preview: bool = False,
) -> Message:
"""
Edit a message.
@@ -1247,6 +1247,7 @@ class Client:
title: Optional[str] = None,
performer: Optional[str] = None,
emoji: Optional[str] = None,
emoji_sticker: Optional[str] = None,
width: Optional[int] = None,
height: Optional[int] = None,
round: bool = False,
@@ -1396,6 +1397,7 @@ class Client:
title=title,
performer=performer,
emoji=emoji,
emoji_sticker=emoji_sticker,
width=width,
height=height,
round=round,
@@ -1425,14 +1427,7 @@ class Client:
:param text: See :ref:`formatting`.
:param markdown: See :ref:`formatting`.
:param html: See :ref:`formatting`.
:param link_preview:
Whether the link preview is allowed.
Setting this to :data:`True` does not guarantee a preview.
Telegram must be able to generate a preview from the first link in the message text.
To regenerate the preview, send the link to `@WebpageBot <https://t.me/WebpageBot>`_.
:param link_preview: See :ref:`formatting`.
:param reply_to:
The message identifier of the message to reply to.
@@ -1578,6 +1573,58 @@ class Client:
def set_default_rights(self, chat: ChatLike, user: ChatLike) -> None:
set_default_rights(self, chat, user)
async def edit_draft(
self,
chat: ChatLike,
text: Optional[str] = None,
*,
markdown: Optional[str] = None,
html: Optional[str] = None,
link_preview: bool = False,
reply_to: Optional[int] = None,
) -> Draft:
"""
Set a draft message in a chat.
This can also be used to clear the draft by setting the text to an empty string ``""``.
:param chat:
The :term:`chat` where the draft will be saved to.
:param text: See :ref:`formatting`.
:param markdown: See :ref:`formatting`.
:param html: See :ref:`formatting`.
:param link_preview: See :ref:`formatting`.
:param reply_to:
The message identifier of the message to reply to.
:return: The created draft.
.. rubric:: Example
.. code-block:: python
# Edit message to have text without formatting
await client.edit_message(chat, msg_id, text='New text')
# Remove the link preview without changing the text
await client.edit_message(chat, msg_id, link_preview=False)
.. seealso::
:meth:`telethon.types.Message.edit`
"""
return await edit_draft(
self,
chat,
text,
markdown=markdown,
html=html,
link_preview=link_preview,
reply_to=reply_to,
)
def set_handler_filter(
self,
handler: Callable[[Event], Awaitable[Any]],

View File

@@ -1,10 +1,12 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import time
from typing import TYPE_CHECKING, Optional
from ...tl import functions, types
from ..types import AsyncList, ChatLike, Dialog, Draft
from ..utils import build_chat_map
from ..utils import build_chat_map, build_msg_map
from .messages import parse_message
if TYPE_CHECKING:
from .client import Client
@@ -40,8 +42,11 @@ class DialogList(AsyncList[Dialog]):
assert isinstance(result, (types.messages.Dialogs, types.messages.DialogsSlice))
chat_map = build_chat_map(result.users, result.chats)
msg_map = build_msg_map(self._client, result.messages, chat_map)
self._buffer.extend(Dialog._from_raw(d, chat_map) for d in result.dialogs)
self._buffer.extend(
Dialog._from_raw(self._client, d, chat_map, msg_map) for d in result.dialogs
)
def get_dialogs(self: Client) -> AsyncList[Dialog]:
@@ -93,7 +98,7 @@ class DraftList(AsyncList[Draft]):
chat_map = build_chat_map(result.users, result.chats)
self._buffer.extend(
Draft._from_raw(u, chat_map)
Draft._from_raw_update(self._client, u, chat_map)
for u in result.updates
if isinstance(u, types.UpdateDraftMessage)
)
@@ -104,3 +109,47 @@ class DraftList(AsyncList[Draft]):
def get_drafts(self: Client) -> AsyncList[Draft]:
return DraftList(self)
async def edit_draft(
self: Client,
chat: ChatLike,
text: Optional[str] = None,
*,
markdown: Optional[str] = None,
html: Optional[str] = None,
link_preview: bool = False,
reply_to: Optional[int] = None,
) -> Draft:
packed = await self._resolve_to_packed(chat)
peer = (await self._resolve_to_packed(chat))._to_input_peer()
message, entities = parse_message(
text=text, markdown=markdown, html=html, allow_empty=False
)
assert isinstance(message, str)
result = await self(
functions.messages.save_draft(
no_webpage=not link_preview,
reply_to_msg_id=reply_to,
top_msg_id=None,
peer=peer,
message=message,
entities=entities,
)
)
assert result
return Draft._from_raw(
client=self,
peer=packed._to_peer(),
top_msg_id=0,
draft=types.DraftMessage(
no_webpage=not link_preview,
reply_to_msg_id=reply_to,
message=message,
entities=entities,
date=int(time.time()),
),
chat_map={},
)

View File

@@ -46,7 +46,7 @@ async def send_message(
*,
markdown: Optional[str] = None,
html: Optional[str] = None,
link_preview: Optional[bool] = None,
link_preview: bool = False,
reply_to: Optional[int] = None,
) -> Message:
packed = await self._resolve_to_packed(chat)
@@ -99,51 +99,29 @@ async def send_message(
)
)
if isinstance(result, types.UpdateShortSentMessage):
return Message._from_raw(
return Message._from_defaults(
self,
types.Message(
out=result.out,
mentioned=False,
media_unread=False,
silent=False,
post=False,
from_scheduled=False,
legacy=False,
edit_hide=False,
pinned=False,
noforwards=False,
id=result.id,
from_id=types.PeerUser(user_id=self._session.user.id)
if self._session.user
else None,
peer_id=packed._to_peer(),
fwd_from=None,
via_bot_id=None,
reply_to=types.MessageReplyHeader(
reply_to_scheduled=False,
forum_topic=False,
reply_to_msg_id=reply_to,
reply_to_peer_id=None,
reply_to_top_id=None,
)
if reply_to
else None,
date=result.date,
message=message if isinstance(message, str) else (message.text or ""),
media=result.media,
reply_markup=None,
entities=result.entities,
views=None,
forwards=None,
replies=None,
edit_date=None,
post_author=None,
grouped_id=None,
reactions=None,
restriction_reason=None,
ttl_period=result.ttl_period,
),
{},
out=result.out,
id=result.id,
from_id=types.PeerUser(user_id=self._session.user.id)
if self._session.user
else None,
peer_id=packed._to_peer(),
reply_to=types.MessageReplyHeader(
reply_to_scheduled=False,
forum_topic=False,
reply_to_msg_id=reply_to,
reply_to_peer_id=None,
reply_to_top_id=None,
)
if reply_to
else None,
date=result.date,
message=message if isinstance(message, str) else (message.text or ""),
media=result.media,
entities=result.entities,
ttl_period=result.ttl_period,
)
else:
return self._build_message_map(result, peer).with_random_id(random_id)
@@ -157,7 +135,7 @@ async def edit_message(
text: Optional[str] = None,
markdown: Optional[str] = None,
html: Optional[str] = None,
link_preview: Optional[bool] = None,
link_preview: bool = False,
) -> Message:
peer = (await self._resolve_to_packed(chat))._to_input_peer()
message, entities = parse_message(

View File

@@ -52,7 +52,7 @@ class Group(Chat, metaclass=NoPublicConstructor):
This property is always present, but may be the empty string.
"""
return self._raw.title
return getattr(self._raw, "title", None) or ""
@property
def username(self) -> Optional[str]:

View File

@@ -1,9 +1,17 @@
from typing import Dict, Self
from __future__ import annotations
from ...tl import abcs
from typing import TYPE_CHECKING, Dict, Optional, Self, Union
from ...tl import abcs, types
from ..utils import peer_id
from .chat import Chat
from .draft import Draft
from .message import Message
from .meta import NoPublicConstructor
if TYPE_CHECKING:
from ..client import Client
class Dialog(metaclass=NoPublicConstructor):
"""
@@ -16,12 +24,76 @@ class Dialog(metaclass=NoPublicConstructor):
You can obtain dialogs with methods such as :meth:`telethon.Client.get_dialogs`.
"""
__slots__ = ("_raw", "_chat_map")
def __init__(self, raw: abcs.Dialog, chat_map: Dict[int, Chat]) -> None:
def __init__(
self,
client: Client,
raw: Union[types.Dialog, types.DialogFolder],
chat_map: Dict[int, Chat],
msg_map: Dict[int, Message],
) -> None:
self._client = client
self._raw = raw
self._chat_map = chat_map
self._msg_map = msg_map
@classmethod
def _from_raw(cls, dialog: abcs.Dialog, chat_map: Dict[int, Chat]) -> Self:
return cls._create(dialog, chat_map)
def _from_raw(
cls,
client: Client,
dialog: abcs.Dialog,
chat_map: Dict[int, Chat],
msg_map: Dict[int, Message],
) -> Self:
assert isinstance(dialog, (types.Dialog, types.DialogFolder))
return cls._create(client, dialog, chat_map, msg_map)
@property
def chat(self) -> Chat:
"""
The chat where messages are sent in this dialog.
"""
return self._chat_map[peer_id(self._raw.peer)]
@property
def draft(self) -> Optional[Draft]:
"""
The message draft within this dialog, if any.
This property does not update when the draft changes.
"""
if isinstance(self._raw, types.Dialog) and self._raw.draft:
return Draft._from_raw(
self._client,
self._raw.peer,
self._raw.top_message,
self._raw.draft,
self._chat_map,
)
else:
return None
@property
def latest_message(self) -> Optional[Message]:
"""
The latest message sent or received in this dialog, if any.
This property does not update when new messages arrive.
"""
return self._msg_map.get(self._raw.top_message)
@property
def unread_count(self) -> int:
"""
The amount of unread messages in this dialog.
This property does not update when messages are read or sent.
"""
if isinstance(self._raw, types.Dialog):
return self._raw.unread_count
elif isinstance(self._raw, types.DialogPeerFolder):
return (
self._raw.unread_unmuted_messages_count
+ self._raw.unread_muted_messages_count
)
else:
raise RuntimeError("unexpected case")

View File

@@ -1,9 +1,19 @@
from typing import Dict, Self
from __future__ import annotations
from ...tl import types
import datetime
from typing import TYPE_CHECKING, Dict, Optional, Self
from ...session import PackedChat
from ...tl import abcs, functions, types
from ..parsers import generate_html_message, generate_markdown_message
from ..utils import expand_peer, generate_random_id, peer_id
from .chat import Chat
from .message import Message
from .meta import NoPublicConstructor
if TYPE_CHECKING:
from ..client import Client
class Draft(metaclass=NoPublicConstructor):
"""
@@ -12,19 +22,226 @@ class Draft(metaclass=NoPublicConstructor):
You can obtain drafts with methods such as :meth:`telethon.Client.get_drafts`.
"""
__slots__ = ("_raw", "_chat_map")
def __init__(
self, raw: types.UpdateDraftMessage, chat_map: Dict[int, Chat]
self,
client: Client,
peer: abcs.Peer,
top_msg_id: Optional[int],
raw: abcs.DraftMessage,
chat_map: Dict[int, Chat],
) -> None:
assert isinstance(raw, (types.DraftMessage, types.DraftMessageEmpty))
self._client = client
self._peer = peer
self._raw = raw
self._top_msg_id = top_msg_id
self._chat_map = chat_map
@classmethod
def _from_raw(
cls, draft: types.UpdateDraftMessage, chat_map: Dict[int, Chat]
def _from_raw_update(
cls, client: Client, draft: types.UpdateDraftMessage, chat_map: Dict[int, Chat]
) -> Self:
return cls._create(draft, chat_map)
return cls._create(client, draft.peer, draft.top_msg_id, draft.draft, chat_map)
@classmethod
def _from_raw(
cls,
client: Client,
peer: abcs.Peer,
top_msg_id: int,
draft: abcs.DraftMessage,
chat_map: Dict[int, Chat],
) -> Self:
return cls._create(client, peer, top_msg_id, draft, chat_map)
@property
def chat(self) -> Chat:
"""
The chat where the draft will be sent to.
"""
return self._chat_map.get(peer_id(self._peer)) or expand_peer(
self._peer, broadcast=None
)
@property
def link_preview(self) -> bool:
"""
:data:`True` if the link preview is allowed to exist when sending the message.
"""
return not getattr(self._raw, "no_webpage", False)
@property
def replied_message_id(self) -> Optional[int]:
"""
Get the message identifier of message this draft will reply to once sent.
"""
return getattr(self._raw, "reply_to_msg_id") or None
@property
def text(self) -> Optional[str]:
"""
The :attr:`~Message.text` of the message that will be sent.
"""
return getattr(self._raw, "message", None)
@property
def text_html(self) -> Optional[str]:
"""
The :attr:`~Message.text_html` of the message that will be sent.
"""
if text := getattr(self._raw, "message", None):
return generate_html_message(
text, getattr(self._raw, "entities", None) or []
)
else:
return None
@property
def text_markdown(self) -> Optional[str]:
"""
The :attr:`~Message.text_markdown` of the message that will be sent.
"""
if text := getattr(self._raw, "message", None):
return generate_markdown_message(
text, getattr(self._raw, "entities", None) or []
)
else:
return None
@property
def date(self) -> Optional[datetime.datetime]:
"""
The date when the draft was last updated.
"""
date = getattr(self._raw, "date", None)
return (
datetime.datetime.fromtimestamp(date, tz=datetime.timezone.utc)
if date is not None
else None
)
async def edit(
self,
text: Optional[str] = None,
*,
markdown: Optional[str] = None,
html: Optional[str] = None,
link_preview: bool = False,
reply_to: Optional[int] = None,
) -> Draft:
"""
Replace the current draft with a new one.
:param text: See :ref:`formatting`.
:param markdown: See :ref:`formatting`.
:param html: See :ref:`formatting`.
:param link_preview: See :ref:`formatting`.
:param reply_to:
The message identifier of the message to reply to.
:return: The edited draft.
.. rubric:: Example
.. code-block:: python
new_draft = await old_draft.edit('new text', link_preview=False)
"""
return await self._client.edit_draft(
await self._packed_chat(),
text,
markdown=markdown,
html=html,
link_preview=link_preview,
reply_to=reply_to,
)
async def _packed_chat(self) -> PackedChat:
packed = None
if chat := self._chat_map.get(peer_id(self._peer)):
packed = chat.pack()
if packed is None:
packed = await self._client.resolve_to_packed(peer_id(self._peer))
return packed
async def send(self) -> Message:
"""
Send the contents of this draft to the chat.
The draft will be cleared after being sent.
:return: The sent message.
.. rubric:: Example
.. code-block:: python
await draft.send(clear=False)
"""
packed = await self._packed_chat()
peer = packed._to_input_peer()
reply_to = self.replied_message_id
message = getattr(self._raw, "message", "")
entities = getattr(self._raw, "entities", None)
random_id = generate_random_id()
result = await self._client(
functions.messages.send_message(
no_webpage=not self.link_preview,
silent=False,
background=False,
clear_draft=True,
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,
message=message,
random_id=random_id,
reply_markup=None,
entities=entities,
schedule_date=None,
send_as=None,
)
)
if isinstance(result, types.UpdateShortSentMessage):
return Message._from_defaults(
self._client,
{},
out=result.out,
id=result.id,
from_id=types.PeerUser(user_id=self._client._session.user.id)
if self._client._session.user
else None,
peer_id=packed._to_peer(),
reply_to=types.MessageReplyHeader(
reply_to_scheduled=False,
forum_topic=False,
reply_to_msg_id=reply_to,
reply_to_peer_id=None,
reply_to_top_id=None,
)
if reply_to
else None,
date=result.date,
message=message,
media=result.media,
entities=result.entities,
ttl_period=result.ttl_period,
)
else:
return self._client._build_message_map(result, peer).with_random_id(
random_id
)
async def delete(self) -> None:
pass
"""
Clear the contents of this draft to delete it.
"""
await self.edit("")

View File

@@ -1,11 +1,11 @@
from __future__ import annotations
import datetime
from typing import TYPE_CHECKING, Dict, Optional, Self, Union
from typing import TYPE_CHECKING, Any, Dict, Optional, Self, Union
from ...tl import abcs, types
from ..parsers import generate_html_message, generate_markdown_message
from ..utils import expand_peer, peer_id
from ..utils import adapt_date, expand_peer, peer_id
from .chat import Chat, ChatLike
from .file import File
from .meta import NoPublicConstructor
@@ -40,6 +40,52 @@ class Message(metaclass=NoPublicConstructor):
) -> Self:
return cls._create(client, message, chat_map)
@classmethod
def _from_defaults(
cls,
client: Client,
chat_map: Dict[int, Chat],
id: int,
peer_id: abcs.Peer,
date: int,
message: str,
**kwargs: Any,
) -> Self:
default_kwargs: Dict[str, Any] = {
"out": False,
"mentioned": False,
"media_unread": False,
"silent": False,
"post": False,
"from_scheduled": False,
"legacy": False,
"edit_hide": False,
"pinned": False,
"noforwards": False,
"id": id,
"from_id": None,
"peer_id": peer_id,
"fwd_from": None,
"via_bot_id": None,
"reply_to": None,
"date": date,
"message": message,
"media": None,
"reply_markup": None,
"entities": None,
"views": None,
"forwards": None,
"replies": None,
"edit_date": None,
"post_author": None,
"grouped_id": None,
"reactions": None,
"restriction_reason": None,
"ttl_period": None,
}
default_kwargs.update(kwargs)
return cls._create(client, types.Message(**default_kwargs), chat_map)
@property
def id(self) -> int:
"""
@@ -86,12 +132,7 @@ class Message(metaclass=NoPublicConstructor):
@property
def date(self) -> Optional[datetime.datetime]:
date = getattr(self._raw, "date", None)
return (
datetime.datetime.fromtimestamp(date, tz=datetime.timezone.utc)
if date is not None
else None
)
return adapt_date(getattr(self._raw, "date", None))
@property
def chat(self) -> Chat:
@@ -236,7 +277,7 @@ class Message(metaclass=NoPublicConstructor):
text: Optional[str] = None,
markdown: Optional[str] = None,
html: Optional[str] = None,
link_preview: Optional[bool] = None,
link_preview: bool = False,
) -> Message:
"""
Alias for :meth:`telethon.Client.edit_message`.

View File

@@ -1,11 +1,17 @@
from __future__ import annotations
import datetime
import itertools
import sys
import time
from collections import defaultdict
from typing import DefaultDict, Dict, List, Optional, Union
from typing import TYPE_CHECKING, DefaultDict, Dict, List, Optional, Union
from ..tl import abcs, types
from .types import Channel, Chat, Group, User
from .types import Channel, Chat, Group, Message, User
if TYPE_CHECKING:
from .client import Client
_last_id = 0
@@ -51,6 +57,15 @@ def build_chat_map(users: List[abcs.User], chats: List[abcs.Chat]) -> Dict[int,
return result
def build_msg_map(
client: Client, messages: List[abcs.Message], chat_map: Dict[int, Chat]
) -> Dict[int, Message]:
return {
msg.id: msg
for msg in (Message._from_raw(client, m, chat_map) for m in messages)
}
def peer_id(peer: abcs.Peer) -> int:
if isinstance(peer, types.PeerUser):
return peer.user_id
@@ -83,3 +98,11 @@ def expand_peer(peer: abcs.Peer, *, broadcast: Optional[bool]) -> Chat:
return Channel._from_raw(channel) if broadcast else Group._from_raw(channel)
else:
raise RuntimeError("unexpected case")
def adapt_date(date: Optional[int]) -> Optional[datetime.datetime]:
return (
datetime.datetime.fromtimestamp(date, tz=datetime.timezone.utc)
if date is not None
else None
)

View File

@@ -208,7 +208,7 @@ class Encrypted(Mtp):
)
if self._msg_count == 1:
container_msg_id = Single
container_msg_id: Union[Type[Single], int] = Single
else:
container_msg_id = self._get_new_msg_id()
self._buffer[HEADER_LEN : HEADER_LEN + CONTAINER_HEADER_LEN] = struct.pack(