Continue implementation

This commit is contained in:
Lonami Exo 2023-11-08 14:07:33 +01:00
parent 4cc6ecc39b
commit f9435aa1f6
14 changed files with 267 additions and 124 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

@ -118,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,
@ -683,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.
@ -1200,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`.

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

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

@ -281,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`.

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