diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 275b5782..04016f8c 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -5,6 +5,8 @@ build:
os: ubuntu-22.04
tools:
python: "3.11"
+ apt_packages:
+ - graphviz
sphinx:
configuration: client/doc/conf.py
diff --git a/client/doc/concepts/botapi-vs-mtproto.rst b/client/doc/concepts/botapi-vs-mtproto.rst
index cff0b700..7bd9cdfb 100644
--- a/client/doc/concepts/botapi-vs-mtproto.rst
+++ b/client/doc/concepts/botapi-vs-mtproto.rst
@@ -318,7 +318,7 @@ In Telethon:
.. code-block:: python
from telethon import Client, events
- from telethon.events.filters import Any, Command, TextOnly
+ from telethon.events.filters import Any, Command, Media
bot = Client('bot', api_id, api_hash)
# Handle '/start' and '/help'
@@ -329,8 +329,8 @@ In Telethon:
I am here to echo your kind words back to you. Just say anything nice and I'll say the exact same thing to you!\
""")
- # Handle all other messages with only 'text'
- @bot.on(events.NewMessage, TextOnly())
+ # Handle all other messages without media (negating the filter using ~)
+ @bot.on(events.NewMessage, ~Media())
async def echo_message(message: NewMessage):
await message.reply(message.text)
diff --git a/client/doc/concepts/messages.rst b/client/doc/concepts/messages.rst
index bdfaf5f1..a3c23ffc 100644
--- a/client/doc/concepts/messages.rst
+++ b/client/doc/concepts/messages.rst
@@ -79,7 +79,7 @@ Note that `CommonMark's markdown `_ is not fully compat
```
HTML is also not fully compatible with :term:`HTTP Bot API`'s
-`MarkdownV2 style `_,
+`HTML style `_,
and instead favours more standard `HTML elements `_:
* ``strong`` and ``b`` for **bold**.
diff --git a/client/src/telethon/_impl/client/client/client.py b/client/src/telethon/_impl/client/client/client.py
index 7de3154c..a4cf6acd 100644
--- a/client/src/telethon/_impl/client/client/client.py
+++ b/client/src/telethon/_impl/client/client/client.py
@@ -118,6 +118,7 @@ from .updates import (
set_handler_filter,
)
from .users import (
+ get_chats,
get_contacts,
get_me,
input_to_peer,
@@ -683,6 +684,40 @@ class Client:
"""
return get_admin_log(self, chat)
+ async def get_chats(self, chats: Sequence[ChatLike]) -> List[Chat]:
+ """
+ Get the latest basic information about the given chats.
+
+ This method is most commonly used to turn one or more :class:`~types.PackedChat` into the original :class:`~types.Chat`.
+ This includes users, groups and broadcast channels.
+
+ :param chats:
+ The users, groups or channels to fetch.
+
+ :return: The fetched chats.
+
+ .. rubric:: Example
+
+ .. code-block:: python
+
+ # Retrieve a PackedChat from somewhere
+ packed_user = my_database.get_packed_winner()
+
+ # Fetch it
+ users = await client.get_chats([packed_user])
+ user = users[0] # user will be a User if our packed_user was a user
+
+ # Notify the user they won, using their current full name in the message
+ await client.send_message(packed_user, f'Congratulations {user.name}, you won!')
+
+ .. caution::
+
+ This method supports being called with anything that looks like a chat, like every other method.
+ However, calling it with usernames or phone numbers will fetch the chats twice.
+ If that's the case, consider using :meth:`resolve_username` or :meth:`get_contacts` instead.
+ """
+ return await get_chats(self, chats)
+
def get_contacts(self) -> AsyncList[User]:
"""
Get the users in your contact list.
@@ -1200,28 +1235,6 @@ class Client:
"""
return await request_login_code(self, phone)
- async def resolve_to_packed(self, chat: ChatLike) -> PackedChat:
- """
- Resolve a :term:`chat` and return a compact, reusable reference to it.
-
- :param chat:
- The :term:`chat` to resolve.
-
- :return: An efficient, reusable version of the input.
-
- .. rubric:: Example
-
- .. code-block:: python
-
- friend = await client.resolve_to_packed('@cat')
- # Now you can use `friend` to get or send messages, files...
-
- .. seealso::
-
- In-depth explanation for :doc:`/concepts/chats`.
- """
- return await resolve_to_packed(self, chat)
-
async def resolve_username(self, username: str) -> Chat:
"""
Resolve a username into a :term:`chat`.
diff --git a/client/src/telethon/_impl/client/client/updates.py b/client/src/telethon/_impl/client/client/updates.py
index d0d8bb6d..486bdc92 100644
--- a/client/src/telethon/_impl/client/client/updates.py
+++ b/client/src/telethon/_impl/client/client/updates.py
@@ -154,5 +154,5 @@ async def dispatch_next(client: Client) -> None:
for handler, filter in handlers:
if not filter or filter(event):
ret = await handler(event)
- if ret is Continue or client._shortcircuit_handlers:
+ if ret is not Continue or client._shortcircuit_handlers:
return
diff --git a/client/src/telethon/_impl/client/client/users.py b/client/src/telethon/_impl/client/client/users.py
index 2345e405..d7b1739a 100644
--- a/client/src/telethon/_impl/client/client/users.py
+++ b/client/src/telethon/_impl/client/client/users.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, List, Optional, Sequence
from ...mtproto import RpcError
from ...session import PackedChat, PackedType
@@ -13,6 +13,7 @@ from ..types import (
Group,
User,
build_chat_map,
+ expand_peer,
peer_id,
)
@@ -73,12 +74,51 @@ async def resolve_username(self: Client, username: str) -> Chat:
)
-async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
+async def get_chats(self: Client, chats: Sequence[ChatLike]) -> List[Chat]:
+ packed_chats: List[PackedChat] = []
+ input_users: List[types.InputUser] = []
+ input_chats: List[int] = []
+ input_channels: List[types.InputChannel] = []
+
+ for chat in chats:
+ packed = await resolve_to_packed(self, chat)
+ if packed.is_user():
+ input_users.append(packed._to_input_user())
+ elif packed.is_chat():
+ input_chats.append(packed.id)
+ else:
+ input_channels.append(packed._to_input_channel())
+
+ users = (
+ (await self(functions.users.get_users(id=input_users))) if input_users else []
+ )
+ groups = (
+ (await self(functions.messages.get_chats(id=input_chats)))
+ if input_chats
+ else []
+ )
+ assert isinstance(groups, types.messages.Chats)
+ channels = (
+ (await self(functions.channels.get_channels(id=input_channels)))
+ if input_channels
+ else []
+ )
+ assert isinstance(channels, types.messages.Chats)
+
+ chat_map = build_chat_map(self, users, groups.chats + channels.chats)
+ return [
+ chat_map.get(chat.id)
+ or expand_peer(self, chat._to_peer(), broadcast=chat.ty == PackedType.BROADCAST)
+ for chat in packed_chats
+ ]
+
+
+async def resolve_to_packed(client: Client, chat: ChatLike) -> PackedChat:
if isinstance(chat, PackedChat):
return chat
if isinstance(chat, (User, Group, Channel)):
- packed = chat.pack() or self._chat_hashes.get(chat.id)
+ packed = chat.pack() or client._chat_hashes.get(chat.id)
if packed is not None:
return packed
@@ -96,11 +136,11 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
if isinstance(chat, types.InputPeerEmpty):
raise ValueError("Cannot resolve chat")
elif isinstance(chat, types.InputPeerSelf):
- if not self._session.user:
+ if not client._session.user:
raise ValueError("Cannot resolve chat")
return PackedChat(
- ty=PackedType.BOT if self._session.user.bot else PackedType.USER,
- id=self._chat_hashes.self_id,
+ ty=PackedType.BOT if client._session.user.bot else PackedType.USER,
+ id=client._chat_hashes.self_id,
access_hash=0,
)
elif isinstance(chat, types.InputPeerChat):
@@ -130,9 +170,9 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
if isinstance(chat, str):
if chat.startswith("+"):
- resolved = await resolve_phone(self, chat)
+ resolved = await resolve_phone(client, chat)
elif chat == "me":
- if me := self._session.user:
+ if me := client._session.user:
return PackedChat(
ty=PackedType.BOT if me.bot else PackedType.USER,
id=me.id,
@@ -141,13 +181,13 @@ async def resolve_to_packed(self: Client, chat: ChatLike) -> PackedChat:
else:
resolved = None
else:
- resolved = await resolve_username(self, username=chat)
+ resolved = await resolve_username(client, username=chat)
if resolved and (packed := resolved.pack()) is not None:
return packed
if isinstance(chat, int):
- packed = self._chat_hashes.get(chat)
+ packed = client._chat_hashes.get(chat)
if packed is None:
raise ValueError("Cannot resolve chat")
return packed
diff --git a/client/src/telethon/_impl/client/events/filters/__init__.py b/client/src/telethon/_impl/client/events/filters/__init__.py
index d3a1c97a..740f1754 100644
--- a/client/src/telethon/_impl/client/events/filters/__init__.py
+++ b/client/src/telethon/_impl/client/events/filters/__init__.py
@@ -1,6 +1,6 @@
from .combinators import All, Any, Filter, Not
-from .common import Chats, Senders
-from .messages import Command, Forward, Incoming, Media, Outgoing, Reply, Text, TextOnly
+from .common import Chats, ChatType, Senders
+from .messages import Command, Forward, Incoming, Media, Outgoing, Reply, Text
__all__ = [
"All",
@@ -8,6 +8,7 @@ __all__ = [
"Filter",
"Not",
"Chats",
+ "ChatType",
"Senders",
"Command",
"Forward",
@@ -16,5 +17,4 @@ __all__ = [
"Outgoing",
"Reply",
"Text",
- "TextOnly",
]
diff --git a/client/src/telethon/_impl/client/events/filters/combinators.py b/client/src/telethon/_impl/client/events/filters/combinators.py
index b8214eb6..a98870e1 100644
--- a/client/src/telethon/_impl/client/events/filters/combinators.py
+++ b/client/src/telethon/_impl/client/events/filters/combinators.py
@@ -1,10 +1,10 @@
import abc
import typing
-from typing import Callable, Tuple
+from typing import Callable, Tuple, TypeAlias
from ..event import Event
-Filter = Callable[[Event], bool]
+Filter: TypeAlias = Callable[[Event], bool]
class Combinable(abc.ABC):
@@ -48,11 +48,12 @@ class Any(Combinable):
"""
Combine multiple filters, returning :data:`True` if any of the filters pass.
- When either filter is *combinable*, you can use the ``|`` operator instead.
+ When either filter is :class:`~telethon._impl.client.events.filters.combinators.Combinable`,
+ you can use the ``|`` operator instead.
.. code-block:: python
- from telethon.filters import Any, Command
+ from telethon.events.filters import Any, Command
@bot.on(events.NewMessage, Any(Command('/start'), Command('/help')))
async def handler(event): ...
@@ -87,11 +88,12 @@ class All(Combinable):
"""
Combine multiple filters, returning :data:`True` if all of the filters pass.
- When either filter is *combinable*, you can use the ``&`` operator instead.
+ When either filter is :class:`~telethon._impl.client.events.filters.combinators.Combinable`,
+ you can use the ``&`` operator instead.
.. code-block:: python
- from telethon.filters import All, Command, Text
+ from telethon.events.filters import All, Command, Text
@bot.on(events.NewMessage, All(Command('/start'), Text(r'\bdata:\w+')))
async def handler(event): ...
@@ -126,11 +128,12 @@ class Not(Combinable):
"""
Negate the output of a single filter, returning :data:`True` if the nested filter does *not* pass.
- When the filter is *combinable*, you can use the ``~`` operator instead.
+ When the filter is :class:`~telethon._impl.client.events.filters.combinators.Combinable`,
+ you can use the ``~`` operator instead.
.. code-block:: python
- from telethon.filters import All, Command
+ from telethon.events.filters import All, Command
@bot.on(events.NewMessage, Not(Command('/start'))
async def handler(event): ...
diff --git a/client/src/telethon/_impl/client/events/filters/common.py b/client/src/telethon/_impl/client/events/filters/common.py
index 8e4b4ca5..3e9d0944 100644
--- a/client/src/telethon/_impl/client/events/filters/common.py
+++ b/client/src/telethon/_impl/client/events/filters/common.py
@@ -1,4 +1,4 @@
-from typing import Literal, Sequence, Tuple, Type, Union
+from typing import Sequence, Set, Type, Union
from ...types import Channel, Group, User
from ..event import Event
@@ -8,20 +8,21 @@ from .combinators import Combinable
class Chats(Combinable):
"""
Filter by ``event.chat.id``, if the event has a chat.
+
+ :param chat_ids: The chat identifiers to filter on.
"""
__slots__ = ("_chats",)
- def __init__(self, chat_id: Union[int, Sequence[int]], *chat_ids: int) -> None:
- self._chats = {chat_id} if isinstance(chat_id, int) else set(chat_id)
- self._chats.update(chat_ids)
+ def __init__(self, chat_ids: Sequence[int]) -> None:
+ self._chats = set(chat_ids)
@property
- def chat_ids(self) -> Tuple[int, ...]:
+ def chat_ids(self) -> Set[int]:
"""
- The chat identifiers this filter is filtering on.
+ A copy of the set of chat identifiers this filter is filtering on.
"""
- return tuple(self._chats)
+ return set(self._chats)
def __call__(self, event: Event) -> bool:
chat = getattr(event, "chat", None)
@@ -32,20 +33,21 @@ class Chats(Combinable):
class Senders(Combinable):
"""
Filter by ``event.sender.id``, if the event has a sender.
+
+ :param sender_ids: The sender identifiers to filter on.
"""
__slots__ = ("_senders",)
- def __init__(self, sender_id: Union[int, Sequence[int]], *sender_ids: int) -> None:
- self._senders = {sender_id} if isinstance(sender_id, int) else set(sender_id)
- self._senders.update(sender_ids)
+ def __init__(self, sender_ids: Sequence[int]) -> None:
+ self._senders = set(sender_ids)
@property
- def sender_ids(self) -> Tuple[int, ...]:
+ def sender_ids(self) -> Set[int]:
"""
- The sender identifiers this filter is filtering on.
+ A copy of the set of sender identifiers this filter is filtering on.
"""
- return tuple(self._senders)
+ return set(self._senders)
def __call__(self, event: Event) -> bool:
sender = getattr(event, "sender", None)
@@ -55,37 +57,38 @@ class Senders(Combinable):
class ChatType(Combinable):
"""
- Filter by chat type, either ``'user'``, ``'group'`` or ``'broadcast'``.
+ Filter by chat type using :func:`isinstance`.
+
+ :param type: The chat type to filter on.
+
+ .. rubric:: Example
+
+ .. code-block:: python
+
+ from telethon import events
+ from telethon.events import filters
+ from telethon.types import Channel
+
+ # Handle only messages from broadcast channels
+ @client.on(events.NewMessage, filters.ChatType(Channel))
+ async def handler(event):
+ print(event.text)
"""
__slots__ = ("_type",)
def __init__(
self,
- type: Union[Literal["user"], Literal["group"], Literal["broadcast"]],
+ type: Type[Union[User, Group, Channel]],
) -> None:
- if type == "user":
- self._type: Union[Type[User], Type[Group], Type[Channel]] = User
- elif type == "group":
- self._type = Group
- elif type == "broadcast":
- self._type = Channel
- else:
- raise TypeError(f"unrecognised chat type: {type}")
+ self._type = type
@property
- def type(self) -> Union[Literal["user"], Literal["group"], Literal["broadcast"]]:
+ def type(self) -> Type[Union[User, Group, Channel]]:
"""
The chat type this filter is filtering on.
"""
- if self._type == User:
- return "user"
- elif self._type == Group:
- return "group"
- elif self._type == Channel:
- return "broadcast"
- else:
- raise RuntimeError("unexpected case")
+ return self._type
def __call__(self, event: Event) -> bool:
sender = getattr(event, "chat", None)
diff --git a/client/src/telethon/_impl/client/events/filters/messages.py b/client/src/telethon/_impl/client/events/filters/messages.py
index 9045e190..70844f36 100644
--- a/client/src/telethon/_impl/client/events/filters/messages.py
+++ b/client/src/telethon/_impl/client/events/filters/messages.py
@@ -21,7 +21,9 @@ class Text(Combinable):
you need to manually perform the check inside the handler instead.
Note that the caption text in messages with media is also searched.
- If you want to filter based on media, use :class:`TextOnly` or :class:`Media`.
+ If you want to filter based on media, use :class:`Media`.
+
+ :param regexp: The regular expression to :func:`re.search` with on the text.
"""
__slots__ = ("_pattern",)
@@ -43,6 +45,8 @@ class Command(Combinable):
filter ``Command('/help')`` will match both ``"/help"`` and ``"/help@bot"``, but not
``"/list"`` or ``"/help@other"``.
+ :param command: The command to match on.
+
.. note::
The leading forward-slash is not automatically added!
@@ -58,7 +62,7 @@ class Command(Combinable):
__slots__ = ("_cmd", "_username")
def __init__(self, command: str) -> None:
- if re.match(r"\s", command):
+ if re.search(r"\s", command):
raise ValueError(f"command cannot contain spaces: {command}")
self._cmd = command
@@ -87,11 +91,7 @@ class Command(Combinable):
class Incoming(Combinable):
"""
- Filter by ``event.incoming``, that is, messages sent from others to the
- logged-in account.
-
- This is not a reliable way to check that the update was not produced by
- the logged-in account in broadcast channels.
+ Filter by ``event.incoming``, that is, messages sent from others to the logged-in account.
"""
__slots__ = ()
@@ -102,11 +102,9 @@ class Incoming(Combinable):
class Outgoing(Combinable):
"""
- Filter by ``event.outgoing``, that is, messages sent from others to the
- logged-in account.
+ Filter by ``event.outgoing``, that is, messages sent from the logged-in account.
- This is not a reliable way to check that the update was not produced by
- the logged-in account in broadcast channels.
+ This is not a reliable way to check that the update was not produced by the logged-in account in broadcast channels.
"""
__slots__ = ()
@@ -117,32 +115,24 @@ class Outgoing(Combinable):
class Forward(Combinable):
"""
- Filter by ``event.forward``.
+ Filter by ``event.forward_info``, that is, messages that have been forwarded from elsewhere.
"""
__slots__ = ()
def __call__(self, event: Event) -> bool:
- return getattr(event, "forward", None) is not None
+ return getattr(event, "forward_info", None) is not None
class Reply(Combinable):
"""
- Filter by ``event.reply``.
+ Filter by ``event.replied_message_id``, that is, messages which are a reply to another message.
"""
__slots__ = ()
def __call__(self, event: Event) -> bool:
- return getattr(event, "reply", None) is not None
-
-
-class TextOnly(Combinable):
- """
- Filter by messages with some text and no media.
-
- Note that link previews are only considered media if they have a photo or document.
- """
+ return getattr(event, "replied_message_id", None) is not None
class Media(Combinable):
@@ -156,6 +146,10 @@ class Media(Combinable):
When you specify one or more media types, *only* those types will be considered.
You can use literal strings or the constants defined by the filter.
+
+ :param types:
+ The media types to filter on.
+ This is all of them if none are specified.
"""
PHOTO = "photo"
diff --git a/client/src/telethon/_impl/client/events/messages.py b/client/src/telethon/_impl/client/events/messages.py
index f5139209..07218d0f 100644
--- a/client/src/telethon/_impl/client/events/messages.py
+++ b/client/src/telethon/_impl/client/events/messages.py
@@ -1,9 +1,9 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Dict, List, Optional, Self
+from typing import TYPE_CHECKING, Dict, List, Optional, Self, Union
from ...tl import abcs, types
-from ..types import Chat, Message
+from ..types import Chat, Message, expand_peer, peer_id
from .event import Event
if TYPE_CHECKING:
@@ -14,6 +14,8 @@ class NewMessage(Event, Message):
"""
Occurs when a new message is sent or received.
+ This event can be treated as the :class:`~telethon.types.Message` itself.
+
.. caution::
Messages sent with the :class:`~telethon.Client` are also caught,
@@ -39,6 +41,8 @@ class NewMessage(Event, Message):
class MessageEdited(Event, Message):
"""
Occurs when a new message is sent or received.
+
+ This event can be treated as the :class:`~telethon.types.Message` itself.
"""
@classmethod
@@ -80,32 +84,96 @@ class MessageDeleted(Event):
else:
return None
+ @property
+ def message_ids(self) -> List[int]:
+ """
+ The message identifiers of the messages that were deleted.
+ """
+ return self._msg_ids
+
+ @property
+ def channel_id(self) -> Optional[int]:
+ """
+ The channel identifier of the supergroup or broadcast channel where the messages were deleted.
+
+ This will be :data:`None` if the messages were deleted anywhere else.
+ """
+ return self._channel_id
+
class MessageRead(Event):
"""
Occurs both when your messages are read by others, and when you read messages.
"""
- def __init__(self, peer: abcs.Peer, max_id: int, out: bool) -> None:
- self._peer = peer
- self._max_id = max_id
- self._out = out
+ def __init__(
+ self,
+ client: Client,
+ update: Union[
+ types.UpdateReadHistoryInbox,
+ types.UpdateReadHistoryOutbox,
+ types.UpdateReadChannelInbox,
+ types.UpdateReadChannelOutbox,
+ ],
+ chat_map: Dict[int, Chat],
+ ) -> None:
+ self._client = client
+ self._raw = update
+ self._chat_map = chat_map
@classmethod
def _try_from_update(
cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat]
) -> Optional[Self]:
- if isinstance(update, types.UpdateReadHistoryInbox):
- return cls._create(update.peer, update.max_id, False)
- elif isinstance(update, types.UpdateReadHistoryOutbox):
- return cls._create(update.peer, update.max_id, True)
- elif isinstance(update, types.UpdateReadChannelInbox):
- return cls._create(
- types.PeerChannel(channel_id=update.channel_id), update.max_id, False
- )
- elif isinstance(update, types.UpdateReadChannelOutbox):
- return cls._create(
- types.PeerChannel(channel_id=update.channel_id), update.max_id, True
- )
+ if isinstance(
+ update,
+ (
+ types.UpdateReadHistoryInbox,
+ types.UpdateReadHistoryOutbox,
+ types.UpdateReadChannelInbox,
+ types.UpdateReadChannelOutbox,
+ ),
+ ):
+ return cls._create(client, update, chat_map)
else:
return None
+
+ def _peer(self) -> abcs.Peer:
+ if isinstance(
+ self._raw, (types.UpdateReadHistoryInbox, types.UpdateReadHistoryOutbox)
+ ):
+ return self._raw.peer
+ else:
+ return types.PeerChannel(channel_id=self._raw.channel_id)
+
+ @property
+ def chat(self) -> Chat:
+ """
+ The :term:`chat` when the messages were read.
+ """
+ peer = self._peer()
+ pid = peer_id(peer)
+ if pid not in self._chat_map:
+ self._chat_map[pid] = expand_peer(
+ self._client, peer, broadcast=getattr(self._raw, "post", None)
+ )
+ return self._chat_map[pid]
+
+ @property
+ def max_message_id_read(self) -> int:
+ """
+ The highest message identifier of the messages that have been marked as read.
+
+ In other words, messages with an identifier below or equal (``<=``) to this value are considered read.
+ Messages with an identifier higher (``>``) to this value are considered unread.
+
+ .. rubric:: Example
+
+ .. code-block:: python
+
+ if message.id <= event.max_message_id_read:
+ print('message is marked as read')
+ else:
+ print('message is not yet marked as read')
+ """
+ return self._raw.max_id
diff --git a/client/src/telethon/_impl/client/types/draft.py b/client/src/telethon/_impl/client/types/draft.py
index d985f654..4568c613 100644
--- a/client/src/telethon/_impl/client/types/draft.py
+++ b/client/src/telethon/_impl/client/types/draft.py
@@ -163,7 +163,7 @@ class Draft(metaclass=NoPublicConstructor):
if chat := self._chat_map.get(peer_id(self._peer)):
packed = chat.pack()
if packed is None:
- packed = await self._client.resolve_to_packed(peer_id(self._peer))
+ packed = await self._client._resolve_to_packed(peer_id(self._peer))
return packed
async def send(self) -> Message:
diff --git a/client/src/telethon/_impl/client/types/message.py b/client/src/telethon/_impl/client/types/message.py
index 2bc95173..4e304509 100644
--- a/client/src/telethon/_impl/client/types/message.py
+++ b/client/src/telethon/_impl/client/types/message.py
@@ -281,6 +281,26 @@ class Message(metaclass=NoPublicConstructor):
return None
+ @property
+ def incoming(self) -> bool:
+ """
+ :data:`True` if the message is incoming.
+ This would mean another user sent it, and the currently logged-in user received it.
+
+ This is usually the opposite of :attr:`outgoing`, although some messages can be neither.
+ """
+ return getattr(self._raw, "out", None) is False
+
+ @property
+ def outgoing(self) -> bool:
+ """
+ :data:`True` if the message is outgoing.
+ This would mean the currently logged-in user sent it.
+
+ This is usually the opposite of :attr:`incoming`, although some messages can be neither.
+ """
+ return getattr(self._raw, "out", None) is True
+
async def get_replied_message(self) -> Optional[Message]:
"""
Alias for :meth:`telethon.Client.get_messages_with_ids`.
diff --git a/client/src/telethon/events/filters.py b/client/src/telethon/events/filters.py
index a4726d9a..93944358 100644
--- a/client/src/telethon/events/filters.py
+++ b/client/src/telethon/events/filters.py
@@ -11,6 +11,7 @@ from .._impl.client.events.filters import (
All,
Any,
Chats,
+ ChatType,
Command,
Filter,
Forward,
@@ -21,13 +22,13 @@ from .._impl.client.events.filters import (
Reply,
Senders,
Text,
- TextOnly,
)
__all__ = [
"All",
"Any",
"Chats",
+ "ChatType",
"Command",
"Filter",
"Forward",
@@ -38,5 +39,4 @@ __all__ = [
"Reply",
"Senders",
"Text",
- "TextOnly",
]