Skip to content

Commit

Permalink
Make filters combinable
Browse files Browse the repository at this point in the history
  • Loading branch information
Lonami committed Nov 2, 2023
1 parent 2def0a1 commit 2992a8e
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 26 deletions.
9 changes: 6 additions & 3 deletions client/doc/concepts/updates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The most common way is using the :meth:`Client.on` decorator to register your ca
bot = Client(...)
@bot.on(events.NewMessage, filters.Command('/start'))
@bot.on(events.NewMessage, filters.Command('/start') | filters.Command('/help'))
async def handler(event: events.NewMessage):
await event.respond('Beep boop!')
Expand All @@ -46,7 +46,11 @@ The first parameter is the :class:`type` of one of the :mod:`telethon.events`, n
The second parameter is optional.
If provided, it must be a callable function that returns :data:`True` if the handler should run.
Built-in filter functions are available in the :mod:`~telethon.events.filters` module.
In this example, :class:`~events.filters.Command` means the handler will be called when the user sends */start* to the bot.
In this example, :class:`~events.filters.Command` means the handler will be called when the user sends */start* or */help* to the bot.

Built-in filter functions are also :class:`~telethon._impl.client.events.filters.combinators.Combinable`.
This means you can use ``|``, ``&`` and the unary ``~`` to combine filters with *or*, *and*, and negate them, respectively.
These operators correspond to :class:`events.filters.Any`, :class:`events.filters.All` and :class:`events.filters.Not`.

When your ``handler`` function is called, it will receive a single parameter, the event.
The event type is the same as the one you defined in the decorator when registering your handler.
Expand Down Expand Up @@ -140,7 +144,6 @@ This makes it very convenient to write custom filters using the :keyword:`lambda
...
Setting priority on handlers
----------------------------

Expand Down
1 change: 1 addition & 0 deletions client/doc/developing/migration-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ There is no longer nested ``class Event`` inside them either.

Instead, the event type itself is what the handler will actually be called with.
Because filters are separate, there is no longer a need for v1 ``@events.register`` either.
It also means you can combine filters with ``&``, ``|`` and ``~``.

Filters are now normal functions that work with any event.
Of course, this doesn't mean all filters make sense for all events.
Expand Down
4 changes: 4 additions & 0 deletions client/doc/modules/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ Private definitions

Generic parameter used by :class:`AsyncList`.

.. currentmodule:: telethon._impl.client.events.filters.combinators

.. autoclass:: Combinable

.. currentmodule:: telethon._impl.client.types.file

.. autoclass:: InFileLike
Expand Down
6 changes: 3 additions & 3 deletions client/src/telethon/_impl/client/events/filters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from .combinators import All, Any, Not
from .common import Chats, Filter, Senders
from .combinators import All, Any, Filter, Not
from .common import Chats, Senders
from .messages import Command, Forward, Incoming, Media, Outgoing, Reply, Text, TextOnly

__all__ = [
"All",
"Any",
"Filter",
"Not",
"Chats",
"Filter",
"Senders",
"Command",
"Forward",
Expand Down
104 changes: 98 additions & 6 deletions client/src/telethon/_impl/client/events/filters/combinators.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,70 @@
from typing import Tuple
import abc
import typing
from typing import Callable, Tuple

from ..event import Event
from .common import Filter

Filter = Callable[[Event], bool]

class Any:

class Combinable(abc.ABC):
"""
Subclass that enables filters to be combined.
* The :func:`bitwise or <operator.or_>` operator ``|`` can be used to combine filters with :class:`Any`.
* The :func:`bitwise and <operator.and_>` operator ``&`` can be used to combine filters with :class:`All`.
* The :func:`bitwise invert <operator.invert>` operator ``~`` can be used to negate a filter with :class:`Not`.
Filters combined this way will be merged.
This means multiple ``|`` or ``&`` will lead to a single :class:`Any` or :class:`All` being used.
Multiple ``~`` will toggle between using :class:`Not` and not using it.
"""

def __or__(self, other: typing.Any) -> Filter:
if not callable(other):
return NotImplemented

lhs = self.filters if isinstance(self, Any) else (self,)
rhs = other.filters if isinstance(other, Any) else (other,)
return Any(*lhs, *rhs) # type: ignore [arg-type]

def __and__(self, other: typing.Any) -> Filter:
if not callable(other):
return NotImplemented

lhs = self.filters if isinstance(self, All) else (self,)
rhs = other.filters if isinstance(other, All) else (other,)
return All(*lhs, *rhs) # type: ignore [arg-type]

def __invert__(self) -> Filter:
return self.filter if isinstance(self, Not) else Not(self) # type: ignore [return-value]

@abc.abstractmethod
def __call__(self, event: Event) -> bool:
pass


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.
.. code-block:: python
from telethon.filters import Any, Command
@bot.on(events.NewMessage, Any(Command('/start'), Command('/help')))
async def handler(event): ...
# equivalent to:
@bot.on(events.NewMessage, Command('/start') | Command('/help'))
async def handler(event): ...
:param filter1: The first filter to check.
:param filter2: The second filter to check if the first one failed.
:param filters: The rest of filters to check if the first and second one failed.
"""

__slots__ = ("_filters",)
Expand All @@ -25,9 +83,27 @@ def __call__(self, event: Event) -> bool:
return any(f(event) for f in self._filters)


class All:
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.
.. code-block:: python
from telethon.filters import All, Command, Text
@bot.on(events.NewMessage, All(Command('/start'), Text(r'\bdata:\w+')))
async def handler(event): ...
# equivalent to:
@bot.on(events.NewMessage, Command('/start') & Text(r'\bdata:\w+'))
async def handler(event): ...
:param filter1: The first filter to check.
:param filter2: The second filter to check.
:param filters: The rest of filters to check.
"""

__slots__ = ("_filters",)
Expand All @@ -46,9 +122,25 @@ def __call__(self, event: Event) -> bool:
return all(f(event) for f in self._filters)


class Not:
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.
.. code-block:: python
from telethon.filters import All, Command
@bot.on(events.NewMessage, Not(Command('/start'))
async def handler(event): ...
# equivalent to:
@bot.on(events.NewMessage, ~Command('/start'))
async def handler(event): ...
:param filter: The filter to negate.
"""

__slots__ = ("_filter",)
Expand All @@ -59,7 +151,7 @@ def __init__(self, filter: Filter) -> None:
@property
def filter(self) -> Filter:
"""
The filters being negated.
The filter being negated.
"""
return self._filter

Expand Down
11 changes: 5 additions & 6 deletions client/src/telethon/_impl/client/events/filters/common.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from typing import Callable, Literal, Sequence, Tuple, Type, Union
from typing import Literal, Sequence, Tuple, Type, Union

from ...types import Channel, Group, User
from ..event import Event
from .combinators import Combinable

Filter = Callable[[Event], bool]


class Chats:
class Chats(Combinable):
"""
Filter by ``event.chat.id``, if the event has a chat.
"""
Expand All @@ -30,7 +29,7 @@ def __call__(self, event: Event) -> bool:
return id in self._chats


class Senders:
class Senders(Combinable):
"""
Filter by ``event.sender.id``, if the event has a sender.
"""
Expand All @@ -54,7 +53,7 @@ def __call__(self, event: Event) -> bool:
return id in self._senders


class ChatType:
class ChatType(Combinable):
"""
Filter by chat type, either ``'user'``, ``'group'`` or ``'broadcast'``.
"""
Expand Down
17 changes: 9 additions & 8 deletions client/src/telethon/_impl/client/events/filters/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
from typing import TYPE_CHECKING, Literal, Optional, Tuple, Union

from ..event import Event
from .combinators import Combinable

if TYPE_CHECKING:
from ...client.client import Client


class Text:
class Text(Combinable):
"""
Filter by ``event.text`` using a *regular expression* pattern.
Expand All @@ -33,7 +34,7 @@ def __call__(self, event: Event) -> bool:
return re.search(self._pattern, text) is not None if text is not None else False


class Command:
class Command(Combinable):
"""
Filter by ``event.text`` to make sure the first word matches the command or
the command + '@' + username, using the username of the logged-in account.
Expand Down Expand Up @@ -84,7 +85,7 @@ def __call__(self, event: Event) -> bool:
return False


class Incoming:
class Incoming(Combinable):
"""
Filter by ``event.incoming``, that is, messages sent from others to the
logged-in account.
Expand All @@ -99,7 +100,7 @@ def __call__(self, event: Event) -> bool:
return getattr(event, "incoming", False)


class Outgoing:
class Outgoing(Combinable):
"""
Filter by ``event.outgoing``, that is, messages sent from others to the
logged-in account.
Expand All @@ -114,7 +115,7 @@ def __call__(self, event: Event) -> bool:
return getattr(event, "outgoing", False)


class Forward:
class Forward(Combinable):
"""
Filter by ``event.forward``.
"""
Expand All @@ -125,7 +126,7 @@ def __call__(self, event: Event) -> bool:
return getattr(event, "forward", None) is not None


class Reply:
class Reply(Combinable):
"""
Filter by ``event.reply``.
"""
Expand All @@ -136,15 +137,15 @@ def __call__(self, event: Event) -> bool:
return getattr(event, "reply", None) is not None


class TextOnly:
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:
class Media(Combinable):
"""
Filter by the media type in the message.
Expand Down

0 comments on commit 2992a8e

Please sign in to comment.