Make filters combinable

This commit is contained in:
Lonami Exo 2023-11-02 01:30:40 +01:00
parent 2def0a169c
commit 2992a8e212
7 changed files with 126 additions and 26 deletions

View File

@ -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!')
@ -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.
@ -140,7 +144,6 @@ This makes it very convenient to write custom filters using the :keyword:`lambda
...
Setting priority on handlers
----------------------------

View File

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

View File

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

View File

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

View File

@ -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",)
@ -25,9 +83,27 @@ class Any:
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",)
@ -46,9 +122,25 @@ class All:
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",)
@ -59,7 +151,7 @@ class Not:
@property
def filter(self) -> Filter:
"""
The filters being negated.
The filter being negated.
"""
return self._filter

View File

@ -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
Filter = Callable[[Event], bool]
from .combinators import Combinable
class Chats:
class Chats(Combinable):
"""
Filter by ``event.chat.id``, if the event has a chat.
"""
@ -30,7 +29,7 @@ class Chats:
return id in self._chats
class Senders:
class Senders(Combinable):
"""
Filter by ``event.sender.id``, if the event has a sender.
"""
@ -54,7 +53,7 @@ class Senders:
return id in self._senders
class ChatType:
class ChatType(Combinable):
"""
Filter by chat type, either ``'user'``, ``'group'`` or ``'broadcast'``.
"""

View File

@ -4,12 +4,13 @@ import re
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.
@ -33,7 +34,7 @@ class Text:
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.
@ -84,7 +85,7 @@ class Command:
return False
class Incoming:
class Incoming(Combinable):
"""
Filter by ``event.incoming``, that is, messages sent from others to the
logged-in account.
@ -99,7 +100,7 @@ class Incoming:
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.
@ -114,7 +115,7 @@ class Outgoing:
return getattr(event, "outgoing", False)
class Forward:
class Forward(Combinable):
"""
Filter by ``event.forward``.
"""
@ -125,7 +126,7 @@ class Forward:
return getattr(event, "forward", None) is not None
class Reply:
class Reply(Combinable):
"""
Filter by ``event.reply``.
"""
@ -136,7 +137,7 @@ class Reply:
return getattr(event, "reply", None) is not None
class TextOnly:
class TextOnly(Combinable):
"""
Filter by messages with some text and no media.
@ -144,7 +145,7 @@ class TextOnly:
"""
class Media:
class Media(Combinable):
"""
Filter by the media type in the message.