mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Remove filters factory, introduce docs translation (#978)
* Rewrite filters * Update README.rst * Fixed tests * Small optimization of the Text filter (TY to @bomzheg) * Remove dataclass slots argument in due to the only Python 3.10 has an slots argument * Fixed mypy * Update tests * Disable Python 3.11 * Fixed #1013: Empty mention should be None instead of empty string. * Added #990 to the changelog * Added #942 to the changelog * Fixed coverage * Update poetry and dependencies * Fixed mypy * Remove deprecated code * Added more tests, update pyproject.toml * Partial update docs * Added initial Docs translation files * Added more changes * Added log message when connection is established in polling process * Fixed action * Disable lint for PyPy * Added changelog for docs translation
This commit is contained in:
parent
94030903ec
commit
f4251382e8
610 changed files with 61738 additions and 1687 deletions
|
|
@ -35,5 +35,5 @@ __all__ = (
|
|||
"flags",
|
||||
)
|
||||
|
||||
__version__ = "3.0.0b4"
|
||||
__version__ = "3.0.0b5"
|
||||
__api_version__ = "6.2"
|
||||
|
|
|
|||
|
|
@ -33,10 +33,12 @@ class Dispatcher(Router):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*, # * - Preventing to pass instance of Bot to the FSM storage
|
||||
storage: Optional[BaseStorage] = None,
|
||||
fsm_strategy: FSMStrategy = FSMStrategy.USER_IN_CHAT,
|
||||
events_isolation: Optional[BaseEventIsolation] = None,
|
||||
disable_fsm: bool = False,
|
||||
name: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
|
|
@ -49,11 +51,16 @@ class Dispatcher(Router):
|
|||
then you should not use storage and events isolation
|
||||
:param kwargs: Other arguments, will be passed as keyword arguments to handlers
|
||||
"""
|
||||
super(Dispatcher, self).__init__(**kwargs)
|
||||
super(Dispatcher, self).__init__(name=name)
|
||||
|
||||
if storage and not isinstance(storage, BaseStorage):
|
||||
raise TypeError(
|
||||
f"FSM storage should be instance of 'BaseStorage' not {type(storage).__name__}"
|
||||
)
|
||||
|
||||
# Telegram API provides originally only one event type - Update
|
||||
# For making easily interactions with events here is registered handler which helps
|
||||
# to separate Update to different event types like Message, CallbackQuery and etc.
|
||||
# to separate Update to different event types like Message, CallbackQuery etc.
|
||||
self.update = self.observers["update"] = TelegramEventObserver(
|
||||
router=self, event_name="update"
|
||||
)
|
||||
|
|
@ -80,7 +87,7 @@ class Dispatcher(Router):
|
|||
self.update.outer_middleware(self.fsm)
|
||||
self.shutdown.register(self.fsm.close)
|
||||
|
||||
self.workflow_data: Dict[str, Any] = {}
|
||||
self.workflow_data: Dict[str, Any] = kwargs
|
||||
self._running_lock = Lock()
|
||||
|
||||
def __getitem__(self, item: str) -> Any:
|
||||
|
|
@ -186,10 +193,12 @@ class Dispatcher(Router):
|
|||
# Request timeout can be lower than session timeout and that's OK.
|
||||
# To prevent false-positive TimeoutError we should wait longer than polling timeout
|
||||
kwargs["request_timeout"] = int(bot.session.timeout + polling_timeout)
|
||||
failed = False
|
||||
while True:
|
||||
try:
|
||||
updates = await bot(get_updates, **kwargs)
|
||||
except Exception as e:
|
||||
failed = True
|
||||
# In cases when Telegram Bot API was inaccessible don't need to stop polling
|
||||
# process because some developers can't make auto-restarting of the script
|
||||
loggers.dispatcher.error("Failed to fetch updates - %s: %s", type(e).__name__, e)
|
||||
|
|
@ -205,7 +214,14 @@ class Dispatcher(Router):
|
|||
|
||||
# In case when network connection was fixed let's reset the backoff
|
||||
# to initial value and then process updates
|
||||
backoff.reset()
|
||||
if failed:
|
||||
loggers.dispatcher.info(
|
||||
"Connection established (tryings = %d, bot id = %d)",
|
||||
backoff.counter,
|
||||
bot.id,
|
||||
)
|
||||
backoff.reset()
|
||||
failed = False
|
||||
|
||||
for update in updates:
|
||||
yield update
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import asyncio
|
||||
import contextvars
|
||||
import inspect
|
||||
import warnings
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from magic_filter import MagicFilter
|
||||
from magic_filter.magic import MagicFilter as OriginalMagicFilter
|
||||
|
||||
from aiogram.dispatcher.flags import extract_flags_from_object
|
||||
from aiogram.filters.base import Filter
|
||||
from aiogram.handlers import BaseHandler
|
||||
from aiogram.utils.magic_filter import MagicFilter
|
||||
from aiogram.utils.warnings import Recommendation
|
||||
|
||||
CallbackType = Callable[..., Any]
|
||||
|
||||
|
|
@ -45,20 +49,33 @@ class CallableMixin:
|
|||
|
||||
@dataclass
|
||||
class FilterObject(CallableMixin):
|
||||
callback: CallbackType
|
||||
magic: Optional[MagicFilter] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# TODO: Make possibility to extract and explain magic from filter object.
|
||||
# Current solution is hard for debugging because the MagicFilter instance can't be extracted
|
||||
if isinstance(self.callback, MagicFilter):
|
||||
# MagicFilter instance is callable but generates only "CallOperation" instead of applying the filter
|
||||
if isinstance(self.callback, OriginalMagicFilter):
|
||||
# MagicFilter instance is callable but generates
|
||||
# only "CallOperation" instead of applying the filter
|
||||
self.magic = self.callback
|
||||
self.callback = self.callback.resolve
|
||||
super().__post_init__()
|
||||
if not isinstance(self.magic, MagicFilter):
|
||||
# Issue: https://github.com/aiogram/aiogram/issues/990
|
||||
warnings.warn(
|
||||
category=Recommendation,
|
||||
message="You are using F provided by magic_filter package directly, "
|
||||
"but it lacks `.as_()` extension."
|
||||
"\n Please change the import statement: from `from magic_filter import F` "
|
||||
"to `from aiogram import F` to silence this warning.",
|
||||
stacklevel=6,
|
||||
)
|
||||
|
||||
super(FilterObject, self).__post_init__()
|
||||
|
||||
if isinstance(self.callback, Filter):
|
||||
self.awaitable = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class HandlerObject(CallableMixin):
|
||||
callback: CallbackType
|
||||
filters: Optional[List[FilterObject]] = None
|
||||
flags: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from inspect import isclass
|
||||
from itertools import chain
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Optional, Tuple, Type
|
||||
|
||||
from pydantic import ValidationError
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
|
||||
|
||||
from aiogram.dispatcher.middlewares.manager import MiddlewareManager
|
||||
from aiogram.filters.base import BaseFilter
|
||||
|
||||
from ...exceptions import FiltersResolveError
|
||||
from ...filters import BUILTIN_FILTERS_SET
|
||||
from ...filters.base import Filter
|
||||
from ...types import TelegramObject
|
||||
from .bases import REJECTED, UNHANDLED, MiddlewareType, SkipHandler
|
||||
from .handler import CallbackType, FilterObject, HandlerObject
|
||||
|
|
@ -33,7 +26,6 @@ class TelegramEventObserver:
|
|||
self.event_name: str = event_name
|
||||
|
||||
self.handlers: List[HandlerObject] = []
|
||||
self.filters: List[Type[BaseFilter]] = []
|
||||
|
||||
self.middleware = MiddlewareManager()
|
||||
self.outer_middleware = MiddlewareManager()
|
||||
|
|
@ -42,63 +34,16 @@ class TelegramEventObserver:
|
|||
# with dummy callback which never will be used
|
||||
self._handler = HandlerObject(callback=lambda: True, filters=[])
|
||||
|
||||
def filter(self, *filters: CallbackType, _stacklevel: int = 2, **bound_filters: Any) -> None:
|
||||
def filter(self, *filters: CallbackType) -> None:
|
||||
"""
|
||||
Register filter for all handlers of this event observer
|
||||
|
||||
:param filters: positional filters
|
||||
:param bound_filters: keyword filters
|
||||
"""
|
||||
resolved_filters = self.resolve_filters(
|
||||
filters, bound_filters, _stacklevel=_stacklevel + 1
|
||||
)
|
||||
if self._handler.filters is None:
|
||||
self._handler.filters = []
|
||||
self._handler.filters.extend(
|
||||
[
|
||||
FilterObject(filter_) # type: ignore
|
||||
for filter_ in chain(
|
||||
resolved_filters,
|
||||
filters,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
def bind_filter(self, bound_filter: Type[BaseFilter]) -> None:
|
||||
"""
|
||||
Register filter class in factory
|
||||
|
||||
:param bound_filter:
|
||||
"""
|
||||
if not isclass(bound_filter) or not issubclass(bound_filter, BaseFilter):
|
||||
raise TypeError(
|
||||
"bound_filter() argument 'bound_filter' must be subclass of BaseFilter"
|
||||
)
|
||||
if bound_filter not in BUILTIN_FILTERS_SET:
|
||||
warnings.warn(
|
||||
category=DeprecationWarning,
|
||||
message="filters factory deprecated and will be removed in 3.0b5,"
|
||||
" use filters directly instead (Example: "
|
||||
f"`{bound_filter.__name__}(<argument>=<value>)` instead of `<argument>=<value>`)",
|
||||
stacklevel=2,
|
||||
)
|
||||
self.filters.append(bound_filter)
|
||||
|
||||
def _resolve_filters_chain(self) -> Generator[Type[BaseFilter], None, None]:
|
||||
"""
|
||||
Get all bounded filters from current observer and from the parents
|
||||
with the same event type without duplicates
|
||||
"""
|
||||
registry: List[Type[BaseFilter]] = []
|
||||
|
||||
for router in reversed(tuple(self.router.chain_head)):
|
||||
observer = router.observers[self.event_name]
|
||||
|
||||
for filter_ in observer.filters:
|
||||
if filter_ in registry:
|
||||
continue
|
||||
yield filter_
|
||||
registry.append(filter_)
|
||||
self._handler.filters.extend([FilterObject(filter_) for filter_ in filters])
|
||||
|
||||
def _resolve_middlewares(self) -> List[MiddlewareType[TelegramObject]]:
|
||||
middlewares: List[MiddlewareType[TelegramObject]] = []
|
||||
|
|
@ -108,112 +53,30 @@ class TelegramEventObserver:
|
|||
|
||||
return middlewares
|
||||
|
||||
def resolve_filters(
|
||||
self,
|
||||
filters: Tuple[CallbackType, ...],
|
||||
full_config: Dict[str, Any],
|
||||
ignore_default: bool = True,
|
||||
_stacklevel: int = 2,
|
||||
) -> List[BaseFilter]:
|
||||
"""
|
||||
Resolve keyword filters via filters factory
|
||||
|
||||
:param filters: positional filters
|
||||
:param full_config: keyword arguments to initialize bounded filters for router/handler
|
||||
:param ignore_default: ignore to resolving filters with only default arguments that are not in full_config
|
||||
"""
|
||||
bound_filters: List[BaseFilter] = []
|
||||
|
||||
if ignore_default and not full_config:
|
||||
return bound_filters
|
||||
|
||||
filter_types = set(type(f) for f in filters)
|
||||
|
||||
validation_errors = []
|
||||
for bound_filter in self._resolve_filters_chain():
|
||||
# skip filter if filter was used as positional filter:
|
||||
if bound_filter in filter_types:
|
||||
continue
|
||||
|
||||
# skip filter with no fields in full_config
|
||||
if ignore_default:
|
||||
full_config_keys = set(full_config.keys())
|
||||
filter_fields = set(bound_filter.__fields__.keys())
|
||||
|
||||
if not full_config_keys.intersection(filter_fields):
|
||||
continue
|
||||
|
||||
# Try to initialize filter.
|
||||
try:
|
||||
f = bound_filter(**full_config)
|
||||
except ValidationError as e:
|
||||
validation_errors.append(e)
|
||||
continue
|
||||
|
||||
# Clean full config to prevent to re-initialize another filter
|
||||
# with the same configuration
|
||||
for key in f.__fields__:
|
||||
full_config.pop(key, None)
|
||||
|
||||
bound_filters.append(f)
|
||||
|
||||
if full_config:
|
||||
possible_cases = []
|
||||
for error in validation_errors:
|
||||
for sum_error in error.errors():
|
||||
if sum_error["loc"][0] in full_config:
|
||||
possible_cases.append(error)
|
||||
break
|
||||
|
||||
raise FiltersResolveError(
|
||||
unresolved_fields=set(full_config.keys()), possible_cases=possible_cases
|
||||
)
|
||||
|
||||
if bound_filters:
|
||||
warnings.warn(
|
||||
category=DeprecationWarning,
|
||||
message="Filters factory deprecated and will be removed in 3.0b5.\n"
|
||||
"Use filters directly, for example instead of "
|
||||
"`@router.message(commands=['help']')` "
|
||||
"use `@router.message(Command(commands=['help'])`",
|
||||
stacklevel=_stacklevel,
|
||||
)
|
||||
return bound_filters
|
||||
|
||||
def register(
|
||||
self,
|
||||
callback: CallbackType,
|
||||
*filters: CallbackType,
|
||||
flags: Optional[Dict[str, Any]] = None,
|
||||
_stacklevel: int = 2,
|
||||
**bound_filters: Any,
|
||||
) -> CallbackType:
|
||||
"""
|
||||
Register event handler
|
||||
"""
|
||||
if flags is None:
|
||||
flags = {}
|
||||
resolved_filters = self.resolve_filters(
|
||||
filters,
|
||||
bound_filters,
|
||||
ignore_default=False,
|
||||
_stacklevel=_stacklevel + 1,
|
||||
)
|
||||
for resolved_filter in resolved_filters:
|
||||
resolved_filter.update_handler_flags(flags=flags)
|
||||
|
||||
for item in filters:
|
||||
if isinstance(item, Filter):
|
||||
item.update_handler_flags(flags=flags)
|
||||
|
||||
self.handlers.append(
|
||||
HandlerObject(
|
||||
callback=callback,
|
||||
filters=[
|
||||
FilterObject(filter_) # type: ignore
|
||||
for filter_ in chain(
|
||||
resolved_filters,
|
||||
filters,
|
||||
)
|
||||
],
|
||||
filters=[FilterObject(filter_) for filter_ in filters],
|
||||
flags=flags,
|
||||
)
|
||||
)
|
||||
|
||||
return callback
|
||||
|
||||
def wrap_outer_middleware(
|
||||
|
|
@ -253,19 +116,15 @@ class TelegramEventObserver:
|
|||
|
||||
def __call__(
|
||||
self,
|
||||
*args: CallbackType,
|
||||
*filters: CallbackType,
|
||||
flags: Optional[Dict[str, Any]] = None,
|
||||
_stacklevel: int = 2,
|
||||
**bound_filters: Any,
|
||||
) -> Callable[[CallbackType], CallbackType]:
|
||||
"""
|
||||
Decorator for registering event handlers
|
||||
"""
|
||||
|
||||
def wrapper(callback: CallbackType) -> CallbackType:
|
||||
self.register(
|
||||
callback, *args, flags=flags, **bound_filters, _stacklevel=_stacklevel + 1
|
||||
)
|
||||
self.register(callback, *filters, flags=flags)
|
||||
return callback
|
||||
|
||||
return wrapper
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, cast
|
||||
|
||||
from ...types import TelegramObject
|
||||
from ...types import TelegramObject, Update
|
||||
from ...types.error_event import ErrorEvent
|
||||
from ..event.bases import UNHANDLED, CancelHandler, SkipHandler
|
||||
from .base import BaseMiddleware
|
||||
|
||||
|
|
@ -26,7 +27,9 @@ class ErrorsMiddleware(BaseMiddleware):
|
|||
raise
|
||||
except Exception as e:
|
||||
response = await self.router.propagate_event(
|
||||
update_type="error", event=event, **data, exception=e
|
||||
update_type="error",
|
||||
event=ErrorEvent(update=cast(Update, event), exception=e),
|
||||
**data,
|
||||
)
|
||||
if response is not UNHANDLED:
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import Any, Dict, Final, Generator, List, Optional, Set, Union
|
||||
|
||||
from aiogram.filters import BUILTIN_FILTERS
|
||||
|
||||
from ..types import TelegramObject
|
||||
from ..utils.warnings import CodeHasNoEffect
|
||||
from .event.bases import REJECTED, UNHANDLED
|
||||
from .event.event import EventObserver
|
||||
from .event.telegram import TelegramEventObserver
|
||||
|
|
@ -25,14 +21,11 @@ class Router:
|
|||
- By decorator - :obj:`@router.<event_type>(<filters, ...>)`
|
||||
"""
|
||||
|
||||
def __init__(self, use_builtin_filters: bool = True, name: Optional[str] = None) -> None:
|
||||
def __init__(self, *, name: Optional[str] = None) -> None:
|
||||
"""
|
||||
|
||||
:param use_builtin_filters: `aiogram` has many builtin filters and you can controll automatic registration of this filters in factory
|
||||
:param name: Optional router name, can be useful for debugging
|
||||
"""
|
||||
|
||||
self.use_builtin_filters = use_builtin_filters
|
||||
self.name = name or hex(id(self))
|
||||
|
||||
self._parent_router: Optional[Router] = None
|
||||
|
|
@ -83,12 +76,6 @@ class Router:
|
|||
"error": self.errors,
|
||||
}
|
||||
|
||||
# Builtin filters
|
||||
if use_builtin_filters:
|
||||
for name, observer in self.observers.items():
|
||||
for builtin_filter in BUILTIN_FILTERS.get(name, ()):
|
||||
observer.bind_filter(builtin_filter)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{type(self).__name__} {self.name!r}"
|
||||
|
||||
|
|
@ -187,15 +174,6 @@ class Router:
|
|||
if parent == self:
|
||||
raise RuntimeError("Circular referencing of Router is not allowed")
|
||||
|
||||
if not self.use_builtin_filters and parent.use_builtin_filters:
|
||||
warnings.warn(
|
||||
f"{type(self).__name__}(use_builtin_filters=False) has no effect"
|
||||
f" for router {self} in due to builtin filters is already registered"
|
||||
f" in parent router",
|
||||
CodeHasNoEffect,
|
||||
stacklevel=3,
|
||||
)
|
||||
|
||||
parent = parent.parent_router
|
||||
|
||||
self._parent_router = router
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
from textwrap import indent
|
||||
from typing import List, Optional, Set
|
||||
|
||||
from pydantic import ValidationError
|
||||
from typing import Optional
|
||||
|
||||
from aiogram.methods import TelegramMethod
|
||||
from aiogram.methods.base import TelegramType
|
||||
|
|
@ -107,17 +104,3 @@ class RestartingTelegram(TelegramServerError):
|
|||
|
||||
class TelegramEntityTooLarge(TelegramNetworkError):
|
||||
url = "https://core.telegram.org/bots/api#sending-files"
|
||||
|
||||
|
||||
class FiltersResolveError(DetailedAiogramError):
|
||||
def __init__(self, unresolved_fields: Set[str], possible_cases: List[ValidationError]) -> None:
|
||||
possible_cases_str = "\n".join(
|
||||
" - " + indent(str(e), " " * 4).lstrip() for e in possible_cases
|
||||
)
|
||||
message = f"Unknown keyword filters: {unresolved_fields}"
|
||||
if possible_cases_str:
|
||||
message += f"\n Possible cases:\n{possible_cases_str}"
|
||||
|
||||
super().__init__(message=message)
|
||||
self.unresolved_fields = unresolved_fields
|
||||
self.possible_cases = possible_cases
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from itertools import chain
|
||||
from typing import Dict, Tuple, Type
|
||||
|
||||
from .base import BaseFilter
|
||||
from .base import Filter
|
||||
from .chat_member_updated import (
|
||||
ADMINISTRATOR,
|
||||
CREATOR,
|
||||
|
|
@ -18,21 +17,21 @@ from .chat_member_updated import (
|
|||
ChatMemberUpdatedFilter,
|
||||
)
|
||||
from .command import Command, CommandObject, CommandStart
|
||||
from .content_types import ContentTypesFilter
|
||||
from .exception import ExceptionMessageFilter, ExceptionTypeFilter
|
||||
from .logic import and_f, invert_f, or_f
|
||||
from .magic_data import MagicData
|
||||
from .state import StateFilter
|
||||
from .text import Text
|
||||
|
||||
BaseFilter = Filter
|
||||
|
||||
__all__ = (
|
||||
"BUILTIN_FILTERS",
|
||||
"Filter",
|
||||
"BaseFilter",
|
||||
"Text",
|
||||
"Command",
|
||||
"CommandObject",
|
||||
"CommandStart",
|
||||
"ContentTypesFilter",
|
||||
"ExceptionMessageFilter",
|
||||
"ExceptionTypeFilter",
|
||||
"StateFilter",
|
||||
|
|
@ -50,90 +49,6 @@ __all__ = (
|
|||
"IS_NOT_MEMBER",
|
||||
"JOIN_TRANSITION",
|
||||
"LEAVE_TRANSITION",
|
||||
"and_f",
|
||||
"or_f",
|
||||
"invert_f",
|
||||
)
|
||||
|
||||
_ALL_EVENTS_FILTERS: Tuple[Type[BaseFilter], ...] = (MagicData,)
|
||||
_TELEGRAM_EVENTS_FILTERS: Tuple[Type[BaseFilter], ...] = (StateFilter,)
|
||||
|
||||
BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = {
|
||||
"message": (
|
||||
Text,
|
||||
Command,
|
||||
ContentTypesFilter,
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
),
|
||||
"edited_message": (
|
||||
Text,
|
||||
Command,
|
||||
ContentTypesFilter,
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
),
|
||||
"channel_post": (
|
||||
Text,
|
||||
ContentTypesFilter,
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
),
|
||||
"edited_channel_post": (
|
||||
Text,
|
||||
ContentTypesFilter,
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
),
|
||||
"inline_query": (
|
||||
Text,
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
),
|
||||
"chosen_inline_result": (
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
),
|
||||
"callback_query": (
|
||||
Text,
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
),
|
||||
"shipping_query": (
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
),
|
||||
"pre_checkout_query": (
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
),
|
||||
"poll": (
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
),
|
||||
"poll_answer": (
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
),
|
||||
"my_chat_member": (
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
ChatMemberUpdatedFilter,
|
||||
),
|
||||
"chat_member": (
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
ChatMemberUpdatedFilter,
|
||||
),
|
||||
"chat_join_request": (
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
),
|
||||
"error": (
|
||||
ExceptionMessageFilter,
|
||||
ExceptionTypeFilter,
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
),
|
||||
}
|
||||
|
||||
BUILTIN_FILTERS_SET = set(chain.from_iterable(BUILTIN_FILTERS.values()))
|
||||
BUILTIN_FILTERS: Dict[str, Tuple[Type[Filter], ...]] = {}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from aiogram.filters.logic import _LogicFilter
|
||||
if TYPE_CHECKING:
|
||||
from aiogram.dispatcher.event.handler import CallbackType, FilterObject
|
||||
|
||||
|
||||
class BaseFilter(BaseModel, ABC, _LogicFilter):
|
||||
class Filter(ABC):
|
||||
"""
|
||||
If you want to register own filters like builtin filters you will need to write subclass
|
||||
of this class with overriding the :code:`__call__`
|
||||
method and adding filter attributes.
|
||||
|
||||
BaseFilter is subclass of :class:`pydantic.BaseModel` that's mean all subclasses of BaseFilter has
|
||||
the validators based on class attributes and custom validator.
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -34,9 +30,42 @@ class BaseFilter(BaseModel, ABC, _LogicFilter):
|
|||
"""
|
||||
pass
|
||||
|
||||
def __invert__(self) -> "_InvertFilter":
|
||||
return invert_f(self)
|
||||
|
||||
def update_handler_flags(self, flags: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Also if you want to extend handler flags with using this filter you should implement this method
|
||||
|
||||
:param flags: existing flags, can be updated directly
|
||||
"""
|
||||
pass
|
||||
|
||||
def _signature_to_string(self, *args: Any, **kwargs: Any) -> str:
|
||||
items = [repr(arg) for arg in args]
|
||||
items.extend([f"{k}={v!r}" for k, v in kwargs.items() if v is not None])
|
||||
|
||||
return f"{type(self).__name__}({', '.join(items)})"
|
||||
|
||||
def __await__(self): # type: ignore # pragma: no cover
|
||||
# Is needed only for inspection and this method is never be called
|
||||
return self.__call__
|
||||
|
||||
|
||||
class _InvertFilter(Filter):
|
||||
__slots__ = ("target",)
|
||||
|
||||
def __init__(self, target: "FilterObject") -> None:
|
||||
self.target = target
|
||||
|
||||
async def __call__(self, *args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]:
|
||||
return not bool(await self.target.call(*args, **kwargs))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"~{self.target.callback}"
|
||||
|
||||
|
||||
def invert_f(target: "CallbackType") -> _InvertFilter:
|
||||
from aiogram.dispatcher.event.handler import FilterObject
|
||||
|
||||
return _InvertFilter(target=FilterObject(target))
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from uuid import UUID
|
|||
from magic_filter import MagicFilter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.filters.base import Filter
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
T = TypeVar("T", bound="CallbackData")
|
||||
|
|
@ -122,11 +122,8 @@ class CallbackData(BaseModel):
|
|||
"""
|
||||
return CallbackQueryFilter(callback_data=cls, rule=rule)
|
||||
|
||||
# class Config:
|
||||
# use_enum_values = True
|
||||
|
||||
|
||||
class CallbackQueryFilter(BaseFilter):
|
||||
class CallbackQueryFilter(Filter):
|
||||
"""
|
||||
This filter helps to handle callback query.
|
||||
|
||||
|
|
@ -134,10 +131,24 @@ class CallbackQueryFilter(BaseFilter):
|
|||
via callback data instance
|
||||
"""
|
||||
|
||||
callback_data: Type[CallbackData]
|
||||
"""Expected type of callback data"""
|
||||
rule: Optional[MagicFilter] = None
|
||||
"""Magic rule"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
callback_data: Type[CallbackData],
|
||||
rule: Optional[MagicFilter] = None,
|
||||
):
|
||||
"""
|
||||
:param callback_data: Expected type of callback data
|
||||
:param rule: Magic rule
|
||||
"""
|
||||
self.callback_data = callback_data
|
||||
self.rule = rule
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._signature_to_string(
|
||||
callback_data=self.callback_data,
|
||||
rule=self.rule,
|
||||
)
|
||||
|
||||
async def __call__(self, query: CallbackQuery) -> Union[Literal[False], Dict[str, Any]]:
|
||||
if not isinstance(query, CallbackQuery) or not query.data:
|
||||
|
|
@ -150,7 +161,3 @@ class CallbackQueryFilter(BaseFilter):
|
|||
if self.rule is None or self.rule.resolve(callback_data):
|
||||
return {"callback_data": callback_data}
|
||||
return False
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
use_enum_values = True
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from typing import Any, Dict, Optional, TypeVar, Union
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.filters.base import Filter
|
||||
from aiogram.types import ChatMember, ChatMemberUpdated
|
||||
|
||||
MarkerT = TypeVar("MarkerT", bound="_MemberStatusMarker")
|
||||
|
|
@ -154,16 +154,21 @@ LEAVE_TRANSITION = ~JOIN_TRANSITION
|
|||
PROMOTED_TRANSITION = (MEMBER | RESTRICTED | LEFT | KICKED) >> ADMINISTRATOR
|
||||
|
||||
|
||||
class ChatMemberUpdatedFilter(BaseFilter):
|
||||
member_status_changed: Union[
|
||||
_MemberStatusMarker,
|
||||
_MemberStatusGroupMarker,
|
||||
_MemberStatusTransition,
|
||||
]
|
||||
"""Accepts the status transition or new status of the member (see usage in docs)"""
|
||||
class ChatMemberUpdatedFilter(Filter):
|
||||
def __init__(
|
||||
self,
|
||||
member_status_changed: Union[
|
||||
_MemberStatusMarker,
|
||||
_MemberStatusGroupMarker,
|
||||
_MemberStatusTransition,
|
||||
],
|
||||
):
|
||||
self.member_status_changed = member_status_changed
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
def __str__(self) -> str:
|
||||
return self._signature_to_string(
|
||||
member_status_changed=self.member_status_changed,
|
||||
)
|
||||
|
||||
async def __call__(self, member_updated: ChatMemberUpdated) -> Union[bool, Dict[str, Any]]:
|
||||
old = member_updated.old_chat_member
|
||||
|
|
|
|||
|
|
@ -2,57 +2,111 @@ from __future__ import annotations
|
|||
|
||||
import re
|
||||
from dataclasses import dataclass, field, replace
|
||||
from typing import TYPE_CHECKING, Any, Dict, Match, Optional, Pattern, Sequence, Tuple, Union, cast
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Match,
|
||||
Optional,
|
||||
Pattern,
|
||||
Sequence,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from magic_filter import MagicFilter
|
||||
from pydantic import Field, validator
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message
|
||||
from aiogram.filters.base import Filter
|
||||
from aiogram.types import BotCommand, Message
|
||||
from aiogram.utils.deep_linking import decode_payload
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiogram import Bot
|
||||
|
||||
CommandPatternType = Union[str, re.Pattern]
|
||||
CommandPatternType = Union[str, re.Pattern, BotCommand]
|
||||
|
||||
|
||||
class CommandException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Command(BaseFilter):
|
||||
class Command(Filter):
|
||||
"""
|
||||
This filter can be helpful for handling commands from the text messages.
|
||||
|
||||
Works only with :class:`aiogram.types.message.Message` events which have the :code:`text`.
|
||||
"""
|
||||
|
||||
commands: Union[Sequence[CommandPatternType], CommandPatternType]
|
||||
"""List of commands (string or compiled regexp patterns)"""
|
||||
commands_prefix: str = "/"
|
||||
"""Prefix for command. Prefix is always is single char but here you can pass all of allowed prefixes,
|
||||
for example: :code:`"/!"` will work with commands prefixed by :code:`"/"` or :code:`"!"`."""
|
||||
commands_ignore_case: bool = False
|
||||
"""Ignore case (Does not work with regexp, use flags instead)"""
|
||||
commands_ignore_mention: bool = False
|
||||
"""Ignore bot mention. By default bot can not handle commands intended for other bots"""
|
||||
command_magic: Optional[MagicFilter] = None
|
||||
"""Validate command object via Magic filter after all checks done"""
|
||||
def __init__(
|
||||
self,
|
||||
*values: CommandPatternType,
|
||||
commands: Optional[Union[Sequence[CommandPatternType], CommandPatternType]] = None,
|
||||
prefix: str = "/",
|
||||
ignore_case: bool = False,
|
||||
ignore_mention: bool = False,
|
||||
magic: Optional[MagicFilter] = None,
|
||||
):
|
||||
"""
|
||||
List of commands (string or compiled regexp patterns)
|
||||
|
||||
:param prefix: Prefix for command.
|
||||
Prefix is always a single char but here you can pass all of allowed prefixes,
|
||||
for example: :code:`"/!"` will work with commands prefixed
|
||||
by :code:`"/"` or :code:`"!"`.
|
||||
:param ignore_case: Ignore case (Does not work with regexp, use flags instead)
|
||||
:param ignore_mention: Ignore bot mention. By default,
|
||||
bot can not handle commands intended for other bots
|
||||
:param magic: Validate command object via Magic filter after all checks done
|
||||
"""
|
||||
if commands is None:
|
||||
commands = []
|
||||
if isinstance(commands, (str, re.Pattern, BotCommand)):
|
||||
commands = [commands]
|
||||
|
||||
if not isinstance(commands, Iterable):
|
||||
raise ValueError(
|
||||
"Command filter only supports str, re.Pattern, BotCommand object"
|
||||
" or their Iterable"
|
||||
)
|
||||
|
||||
items = []
|
||||
for command in (*values, *commands):
|
||||
if isinstance(command, BotCommand):
|
||||
command = command.command
|
||||
if not isinstance(command, (str, re.Pattern)):
|
||||
raise ValueError(
|
||||
"Command filter only supports str, re.Pattern, BotCommand object"
|
||||
" or their Iterable"
|
||||
)
|
||||
items.append(command)
|
||||
|
||||
if not items:
|
||||
raise ValueError("At least one command should be specified")
|
||||
|
||||
self.commands = tuple(items)
|
||||
self.prefix = prefix
|
||||
self.ignore_case = ignore_case
|
||||
self.ignore_mention = ignore_mention
|
||||
self.magic = magic
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._signature_to_string(
|
||||
*self.commands,
|
||||
prefix=self.prefix,
|
||||
ignore_case=self.ignore_case,
|
||||
ignore_mention=self.ignore_mention,
|
||||
magic=self.magic,
|
||||
)
|
||||
|
||||
def update_handler_flags(self, flags: Dict[str, Any]) -> None:
|
||||
commands = flags.setdefault("commands", [])
|
||||
commands.append(self)
|
||||
|
||||
@validator("commands", always=True)
|
||||
def _validate_commands(
|
||||
cls, value: Union[Sequence[CommandPatternType], CommandPatternType]
|
||||
) -> Sequence[CommandPatternType]:
|
||||
if isinstance(value, (str, re.Pattern)):
|
||||
value = [value]
|
||||
return value
|
||||
|
||||
async def __call__(self, message: Message, bot: Bot) -> Union[bool, Dict[str, Any]]:
|
||||
if not isinstance(message, Message):
|
||||
return False
|
||||
|
||||
text = message.text or message.caption
|
||||
if not text:
|
||||
return False
|
||||
|
|
@ -78,15 +132,18 @@ class Command(BaseFilter):
|
|||
# "/command@mention" -> "/", ("command", "@", "mention")
|
||||
prefix, (command, _, mention) = full_command[0], full_command[1:].partition("@")
|
||||
return CommandObject(
|
||||
prefix=prefix, command=command, mention=mention, args=args[0] if args else None
|
||||
prefix=prefix,
|
||||
command=command,
|
||||
mention=mention or None,
|
||||
args=args[0] if args else None,
|
||||
)
|
||||
|
||||
def validate_prefix(self, command: CommandObject) -> None:
|
||||
if command.prefix not in self.commands_prefix:
|
||||
if command.prefix not in self.prefix:
|
||||
raise CommandException("Invalid command prefix")
|
||||
|
||||
async def validate_mention(self, bot: Bot, command: CommandObject) -> None:
|
||||
if command.mention and not self.commands_ignore_mention:
|
||||
if command.mention and not self.ignore_mention:
|
||||
me = await bot.me()
|
||||
if me.username and command.mention.lower() != me.username.lower():
|
||||
raise CommandException("Mention did not match")
|
||||
|
|
@ -119,16 +176,13 @@ class Command(BaseFilter):
|
|||
return command
|
||||
|
||||
def do_magic(self, command: CommandObject) -> Any:
|
||||
if not self.command_magic:
|
||||
if not self.magic:
|
||||
return command
|
||||
result = self.command_magic.resolve(command)
|
||||
result = self.magic.resolve(command)
|
||||
if not result:
|
||||
raise CommandException("Rejected via magic filter")
|
||||
return replace(command, magic_result=result)
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommandObject:
|
||||
|
|
@ -170,10 +224,32 @@ class CommandObject:
|
|||
|
||||
|
||||
class CommandStart(Command):
|
||||
commands: Tuple[str] = Field(("start",), const=True)
|
||||
commands_prefix: str = Field("/", const=True)
|
||||
deep_link: bool = False
|
||||
deep_link_encoded: bool = False
|
||||
def __init__(
|
||||
self,
|
||||
deep_link: bool = False,
|
||||
deep_link_encoded: bool = False,
|
||||
ignore_case: bool = False,
|
||||
ignore_mention: bool = False,
|
||||
magic: Optional[MagicFilter] = None,
|
||||
):
|
||||
super().__init__(
|
||||
"start",
|
||||
prefix="/",
|
||||
ignore_case=ignore_case,
|
||||
ignore_mention=ignore_mention,
|
||||
magic=magic,
|
||||
)
|
||||
self.deep_link = deep_link
|
||||
self.deep_link_encoded = deep_link_encoded
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._signature_to_string(
|
||||
ignore_case=self.ignore_case,
|
||||
ignore_mention=self.ignore_mention,
|
||||
magic=self.magic,
|
||||
deep_link=self.deep_link,
|
||||
deep_link_encoded=self.deep_link_encoded,
|
||||
)
|
||||
|
||||
async def parse_command(self, text: str, bot: Bot) -> CommandObject:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
from typing import Any, Dict, Optional, Sequence, Union
|
||||
|
||||
from pydantic import validator
|
||||
|
||||
from aiogram.types import Message
|
||||
from aiogram.types.message import ContentType
|
||||
|
||||
from .base import BaseFilter
|
||||
|
||||
|
||||
class ContentTypesFilter(BaseFilter):
|
||||
"""
|
||||
Is useful for handling specific types of messages (For example separate text and stickers handlers).
|
||||
"""
|
||||
|
||||
content_types: Union[Sequence[str], str]
|
||||
"""Sequence of allowed content types"""
|
||||
|
||||
@validator("content_types")
|
||||
def _validate_content_types(
|
||||
cls, value: Optional[Union[Sequence[str], str]]
|
||||
) -> Optional[Sequence[str]]:
|
||||
if not value:
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
value = [value]
|
||||
allowed_content_types = set(ContentType.all())
|
||||
bad_content_types = set(value) - allowed_content_types
|
||||
if bad_content_types:
|
||||
raise ValueError(f"Invalid content types {bad_content_types} is not allowed here")
|
||||
return value
|
||||
|
||||
async def __call__(self, message: Message) -> Union[bool, Dict[str, Any]]:
|
||||
return ContentType.ANY in self.content_types or message.content_type in self.content_types
|
||||
|
|
@ -1,51 +1,51 @@
|
|||
import re
|
||||
from typing import Any, Dict, Pattern, Tuple, Type, Union, cast
|
||||
from typing import Any, Dict, Pattern, Type, Union, cast
|
||||
|
||||
from pydantic import validator
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.filters.base import Filter
|
||||
from aiogram.types import TelegramObject
|
||||
from aiogram.types.error_event import ErrorEvent
|
||||
|
||||
|
||||
class ExceptionTypeFilter(BaseFilter):
|
||||
class ExceptionTypeFilter(Filter):
|
||||
"""
|
||||
Allows to match exception by type
|
||||
"""
|
||||
|
||||
exception: Union[Type[Exception], Tuple[Type[Exception]]]
|
||||
"""Exception type(s)"""
|
||||
def __init__(self, *exceptions: Type[Exception]):
|
||||
"""
|
||||
:param exceptions: Exception type(s)
|
||||
"""
|
||||
if not exceptions:
|
||||
raise ValueError("At least one exception type is required")
|
||||
self.exceptions = exceptions
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
async def __call__(
|
||||
self, obj: TelegramObject, exception: Exception
|
||||
) -> Union[bool, Dict[str, Any]]:
|
||||
return isinstance(exception, self.exception)
|
||||
async def __call__(self, obj: TelegramObject) -> Union[bool, Dict[str, Any]]:
|
||||
return isinstance(cast(ErrorEvent, obj).exception, self.exceptions)
|
||||
|
||||
|
||||
class ExceptionMessageFilter(BaseFilter):
|
||||
class ExceptionMessageFilter(Filter):
|
||||
"""
|
||||
Allow to match exception by message
|
||||
"""
|
||||
|
||||
pattern: Union[str, Pattern[str]]
|
||||
"""Regexp pattern"""
|
||||
def __init__(self, pattern: Union[str, Pattern[str]]):
|
||||
"""
|
||||
:param pattern: Regexp pattern
|
||||
"""
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(pattern)
|
||||
self.pattern = pattern
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
@validator("pattern")
|
||||
def _validate_match(cls, value: Union[str, Pattern[str]]) -> Union[str, Pattern[str]]:
|
||||
if isinstance(value, str):
|
||||
return re.compile(value)
|
||||
return value
|
||||
def __str__(self) -> str:
|
||||
return self._signature_to_string(
|
||||
pattern=self.pattern,
|
||||
)
|
||||
|
||||
async def __call__(
|
||||
self, obj: TelegramObject, exception: Exception
|
||||
self,
|
||||
obj: TelegramObject,
|
||||
) -> Union[bool, Dict[str, Any]]:
|
||||
pattern = cast(Pattern[str], self.pattern)
|
||||
result = pattern.match(str(exception))
|
||||
result = self.pattern.match(str(cast(ErrorEvent, obj).exception))
|
||||
if not result:
|
||||
return False
|
||||
return {"match_exception": result}
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiogram.dispatcher.event.handler import CallbackType, FilterObject
|
||||
|
||||
|
||||
class _LogicFilter:
|
||||
__call__: Callable[..., Awaitable[Union[bool, Dict[str, Any]]]]
|
||||
|
||||
def __and__(self, other: "CallbackType") -> "_AndFilter":
|
||||
return and_f(self, other)
|
||||
|
||||
def __or__(self, other: "CallbackType") -> "_OrFilter":
|
||||
return or_f(self, other)
|
||||
|
||||
def __invert__(self) -> "_InvertFilter":
|
||||
return invert_f(self)
|
||||
|
||||
def __await__(self): # type: ignore # pragma: no cover
|
||||
# Is needed only for inspection and this method is never be called
|
||||
return self.__call__
|
||||
|
||||
|
||||
class _InvertFilter(_LogicFilter):
|
||||
__slots__ = ("target",)
|
||||
|
||||
def __init__(self, target: "FilterObject") -> None:
|
||||
self.target = target
|
||||
|
||||
async def __call__(self, *args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]:
|
||||
return not bool(await self.target.call(*args, **kwargs))
|
||||
|
||||
|
||||
class _AndFilter(_LogicFilter):
|
||||
__slots__ = ("targets",)
|
||||
|
||||
def __init__(self, *targets: "FilterObject") -> None:
|
||||
self.targets = targets
|
||||
|
||||
async def __call__(self, *args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]:
|
||||
final_result = {}
|
||||
|
||||
for target in self.targets:
|
||||
result = await target.call(*args, **kwargs)
|
||||
if not result:
|
||||
return False
|
||||
if isinstance(result, dict):
|
||||
final_result.update(result)
|
||||
|
||||
if final_result:
|
||||
return final_result
|
||||
return True
|
||||
|
||||
|
||||
class _OrFilter(_LogicFilter):
|
||||
__slots__ = ("targets",)
|
||||
|
||||
def __init__(self, *targets: "FilterObject") -> None:
|
||||
self.targets = targets
|
||||
|
||||
async def __call__(self, *args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]:
|
||||
for target in self.targets:
|
||||
result = await target.call(*args, **kwargs)
|
||||
if not result:
|
||||
continue
|
||||
if isinstance(result, dict):
|
||||
return result
|
||||
return bool(result)
|
||||
return False
|
||||
|
||||
|
||||
def and_f(target1: "CallbackType", target2: "CallbackType") -> _AndFilter:
|
||||
from aiogram.dispatcher.event.handler import FilterObject
|
||||
|
||||
return _AndFilter(FilterObject(target1), FilterObject(target2))
|
||||
|
||||
|
||||
def or_f(target1: "CallbackType", target2: "CallbackType") -> _OrFilter:
|
||||
from aiogram.dispatcher.event.handler import FilterObject
|
||||
|
||||
return _OrFilter(FilterObject(target1), FilterObject(target2))
|
||||
|
||||
|
||||
def invert_f(target: "CallbackType") -> _InvertFilter:
|
||||
from aiogram.dispatcher.event.handler import FilterObject
|
||||
|
||||
return _InvertFilter(FilterObject(target))
|
||||
|
|
@ -2,17 +2,20 @@ from typing import Any
|
|||
|
||||
from magic_filter import AttrDict, MagicFilter
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.filters.base import Filter
|
||||
from aiogram.types import TelegramObject
|
||||
|
||||
|
||||
class MagicData(BaseFilter):
|
||||
magic_data: MagicFilter
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
class MagicData(Filter):
|
||||
def __init__(self, magic_data: MagicFilter) -> None:
|
||||
self.magic_data = magic_data
|
||||
|
||||
async def __call__(self, event: TelegramObject, *args: Any, **kwargs: Any) -> Any:
|
||||
return self.magic_data.resolve(
|
||||
AttrDict({"event": event, **{k: v for k, v in enumerate(args)}, **kwargs})
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._signature_to_string(
|
||||
magic_data=self.magic_data,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,40 +1,33 @@
|
|||
from inspect import isclass
|
||||
from typing import Any, Dict, Optional, Sequence, Type, Union, cast, no_type_check
|
||||
from typing import Any, Dict, Optional, Sequence, Type, Union, cast
|
||||
|
||||
from pydantic import Field, validator
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.filters.base import Filter
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import TelegramObject
|
||||
|
||||
StateType = Union[str, None, State, StatesGroup, Type[StatesGroup]]
|
||||
|
||||
|
||||
class StateFilter(BaseFilter):
|
||||
class StateFilter(Filter):
|
||||
"""
|
||||
State filter
|
||||
"""
|
||||
|
||||
state: Union[StateType, Sequence[StateType]] = Field(...)
|
||||
def __init__(self, *states: StateType) -> None:
|
||||
if not states:
|
||||
raise ValueError("At least one state is required")
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
self.states = states
|
||||
|
||||
@validator("state")
|
||||
@no_type_check # issubclass breaks things
|
||||
def _validate_state(cls, v: Union[StateType, Sequence[StateType]]) -> Sequence[StateType]:
|
||||
if (
|
||||
isinstance(v, (str, State, StatesGroup))
|
||||
or (isclass(v) and issubclass(v, StatesGroup))
|
||||
or v is None
|
||||
):
|
||||
return [v]
|
||||
return v
|
||||
def __str__(self) -> str:
|
||||
return self._signature_to_string(
|
||||
*self.states,
|
||||
)
|
||||
|
||||
async def __call__(
|
||||
self, obj: Union[TelegramObject], raw_state: Optional[str] = None
|
||||
) -> Union[bool, Dict[str, Any]]:
|
||||
allowed_states = cast(Sequence[StateType], self.state)
|
||||
allowed_states = cast(Sequence[StateType], self.states)
|
||||
for allowed_state in allowed_states:
|
||||
if isinstance(allowed_state, str) or allowed_state is None:
|
||||
if allowed_state == "*" or raw_state == allowed_state:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union
|
||||
|
||||
from pydantic import root_validator
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.filters.base import Filter
|
||||
from aiogram.types import CallbackQuery, InlineQuery, Message, Poll
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -11,7 +9,7 @@ if TYPE_CHECKING:
|
|||
TextType = Union[str, "LazyProxy"]
|
||||
|
||||
|
||||
class Text(BaseFilter):
|
||||
class Text(Filter):
|
||||
"""
|
||||
Is useful for filtering text :class:`aiogram.types.message.Message`,
|
||||
any :class:`aiogram.types.callback_query.CallbackQuery` with `data`,
|
||||
|
|
@ -19,7 +17,7 @@ class Text(BaseFilter):
|
|||
|
||||
.. warning::
|
||||
|
||||
Only one of `text`, `text_contains`, `text_startswith` or `text_endswith` argument can be used at once.
|
||||
Only one of `text`, `contains`, `startswith` or `endswith` argument can be used at once.
|
||||
Any of that arguments can be string, list, set or tuple of strings.
|
||||
|
||||
.. deprecated:: 3.0
|
||||
|
|
@ -27,40 +25,63 @@ class Text(BaseFilter):
|
|||
use :ref:`magic-filter <magic-filters>`. For example do :pycode:`F.text == "text"` instead
|
||||
"""
|
||||
|
||||
text: Optional[Union[Sequence[TextType], TextType]] = None
|
||||
"""Text equals value or one of values"""
|
||||
text_contains: Optional[Union[Sequence[TextType], TextType]] = None
|
||||
"""Text contains value or one of values"""
|
||||
text_startswith: Optional[Union[Sequence[TextType], TextType]] = None
|
||||
"""Text starts with value or one of values"""
|
||||
text_endswith: Optional[Union[Sequence[TextType], TextType]] = None
|
||||
"""Text ends with value or one of values"""
|
||||
text_ignore_case: bool = False
|
||||
"""Ignore case when checks"""
|
||||
def __init__(
|
||||
self,
|
||||
text: Optional[Union[Sequence[TextType], TextType]] = None,
|
||||
*,
|
||||
contains: Optional[Union[Sequence[TextType], TextType]] = None,
|
||||
startswith: Optional[Union[Sequence[TextType], TextType]] = None,
|
||||
endswith: Optional[Union[Sequence[TextType], TextType]] = None,
|
||||
ignore_case: bool = False,
|
||||
):
|
||||
"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
@root_validator
|
||||
def _validate_constraints(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# Validate that only one text filter type is presented
|
||||
used_args = set(
|
||||
key for key, value in values.items() if key != "text_ignore_case" and value is not None
|
||||
:param text: Text equals value or one of values
|
||||
:param contains: Text contains value or one of values
|
||||
:param startswith: Text starts with value or one of values
|
||||
:param endswith: Text ends with value or one of values
|
||||
:param ignore_case: Ignore case when checks
|
||||
"""
|
||||
self._validate_constraints(
|
||||
text=text,
|
||||
contains=contains,
|
||||
startswith=startswith,
|
||||
endswith=endswith,
|
||||
)
|
||||
self.text = self._prepare_argument(text)
|
||||
self.contains = self._prepare_argument(contains)
|
||||
self.startswith = self._prepare_argument(startswith)
|
||||
self.endswith = self._prepare_argument(endswith)
|
||||
self.ignore_case = ignore_case
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self._signature_to_string(
|
||||
text=self.text,
|
||||
contains=self.contains,
|
||||
startswith=self.startswith,
|
||||
endswith=self.endswith,
|
||||
ignore_case=self.ignore_case,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _prepare_argument(
|
||||
cls, value: Optional[Union[Sequence[TextType], TextType]]
|
||||
) -> Optional[Sequence[TextType]]:
|
||||
from aiogram.utils.i18n.lazy_proxy import LazyProxy
|
||||
|
||||
if isinstance(value, (str, LazyProxy)):
|
||||
return [value]
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def _validate_constraints(cls, **values: Any) -> None:
|
||||
# Validate that only one text filter type is presented
|
||||
used_args = set(key for key, value in values.items() if value is not None)
|
||||
if len(used_args) < 1:
|
||||
raise ValueError(
|
||||
"Filter should contain one of arguments: {'text', 'text_contains', 'text_startswith', 'text_endswith'}"
|
||||
)
|
||||
raise ValueError(f"Filter should contain one of arguments: {set(values.keys())}")
|
||||
if len(used_args) > 1:
|
||||
raise ValueError(f"Arguments {used_args} cannot be used together")
|
||||
|
||||
# Convert single value to list
|
||||
for arg in used_args:
|
||||
if isinstance(values[arg], str):
|
||||
values[arg] = [values[arg]]
|
||||
|
||||
return values
|
||||
|
||||
async def __call__(
|
||||
self, obj: Union[Message, CallbackQuery, InlineQuery, Poll]
|
||||
) -> Union[bool, Dict[str, Any]]:
|
||||
|
|
@ -79,30 +100,30 @@ class Text(BaseFilter):
|
|||
|
||||
if not text:
|
||||
return False
|
||||
if self.text_ignore_case:
|
||||
if self.ignore_case:
|
||||
text = text.lower()
|
||||
|
||||
if self.text is not None:
|
||||
equals = list(map(self.prepare_text, self.text))
|
||||
equals = map(self.prepare_text, self.text)
|
||||
return text in equals
|
||||
|
||||
if self.text_contains is not None:
|
||||
contains = list(map(self.prepare_text, self.text_contains))
|
||||
if self.contains is not None:
|
||||
contains = map(self.prepare_text, self.contains)
|
||||
return all(map(text.__contains__, contains))
|
||||
|
||||
if self.text_startswith is not None:
|
||||
startswith = list(map(self.prepare_text, self.text_startswith))
|
||||
if self.startswith is not None:
|
||||
startswith = map(self.prepare_text, self.startswith)
|
||||
return any(map(text.startswith, startswith))
|
||||
|
||||
if self.text_endswith is not None:
|
||||
endswith = list(map(self.prepare_text, self.text_endswith))
|
||||
if self.endswith is not None:
|
||||
endswith = map(self.prepare_text, self.endswith)
|
||||
return any(map(text.endswith, endswith))
|
||||
|
||||
# Impossible because the validator prevents this situation
|
||||
return False # pragma: no cover
|
||||
|
||||
def prepare_text(self, text: str) -> str:
|
||||
if self.text_ignore_case:
|
||||
if self.ignore_case:
|
||||
return str(text).lower()
|
||||
else:
|
||||
return str(text)
|
||||
|
|
|
|||
16
aiogram/types/error_event.py
Normal file
16
aiogram/types/error_event.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from aiogram.types import Update
|
||||
from aiogram.types.base import MutableTelegramObject
|
||||
|
||||
|
||||
class ErrorEvent(MutableTelegramObject):
|
||||
"""
|
||||
Internal event, should be used to receive errors while processing Updates from Telegram
|
||||
"""
|
||||
|
||||
update: Update
|
||||
"""Received update"""
|
||||
exception: Exception
|
||||
"""Exception"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
|
@ -67,7 +67,7 @@ class BufferedInputFile(InputFile):
|
|||
|
||||
:param path: Path to file
|
||||
:param filename: Filename to be propagated to telegram.
|
||||
By default will be parsed from path
|
||||
By default, will be parsed from path
|
||||
:param chunk_size: Uploading chunk size
|
||||
:return: instance of :obj:`BufferedInputFile`
|
||||
"""
|
||||
|
|
@ -95,7 +95,7 @@ class FSInputFile(InputFile):
|
|||
|
||||
:param path: Path to file
|
||||
:param filename: Filename to be propagated to telegram.
|
||||
By default will be parsed from path
|
||||
By default, will be parsed from path
|
||||
:param chunk_size: Uploading chunk size
|
||||
"""
|
||||
if filename is None:
|
||||
|
|
@ -106,10 +106,8 @@ class FSInputFile(InputFile):
|
|||
|
||||
async def read(self, chunk_size: int) -> AsyncGenerator[bytes, None]:
|
||||
async with aiofiles.open(self.path, "rb") as f:
|
||||
chunk = await f.read(chunk_size)
|
||||
while chunk:
|
||||
while chunk := await f.read(chunk_size):
|
||||
yield chunk
|
||||
chunk = await f.read(chunk_size)
|
||||
|
||||
|
||||
class URLInputFile(InputFile):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from ..utils.text_decorations import add_surrogates, remove_surrogates
|
||||
|
|
@ -36,11 +35,3 @@ class MessageEntity(MutableTelegramObject):
|
|||
return remove_surrogates(
|
||||
add_surrogates(text)[self.offset * 2 : (self.offset + self.length) * 2]
|
||||
)
|
||||
|
||||
def extract(self, text: str) -> str:
|
||||
warnings.warn(
|
||||
"Method `MessageEntity.extract(...)` deprecated and will be removed in 3.0b5.\n"
|
||||
" Use `MessageEntity.extract_from(...)` instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return self.extract_from(text=text)
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class Update(TelegramObject):
|
|||
def __hash__(self) -> int:
|
||||
return hash((type(self), self.update_id))
|
||||
|
||||
@property # type: ignore
|
||||
@property
|
||||
@lru_cache()
|
||||
def event_type(self) -> str:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ account to their account on some external service.
|
|||
You can read detailed description in the source:
|
||||
https://core.telegram.org/bots#deep-linking
|
||||
|
||||
We have add some utils to get deep links more handy.
|
||||
We have added some utils to get deep links more handy.
|
||||
|
||||
Basic link example:
|
||||
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ class ItemsList(List[str]):
|
|||
self.extend(other)
|
||||
return self
|
||||
|
||||
__iadd__ = __add__ = __rand__ = __and__ = __ror__ = __or__ = add
|
||||
__iadd__ = __add__ = __rand__ = __and__ = __ror__ = __or__ = add # type: ignore
|
||||
|
||||
|
||||
class OrderedHelperMeta(type):
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@ class AiogramWarning(Warning):
|
|||
pass
|
||||
|
||||
|
||||
class CodeHasNoEffect(AiogramWarning):
|
||||
class Recommendation(AiogramWarning):
|
||||
pass
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue