python-trueconf-bot 1.2.3.dev1__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/METADATA +2 -1
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/RECORD +30 -14
- trueconf/__init__.py +2 -0
- trueconf/_version.py +2 -2
- trueconf/client/bot.py +193 -37
- trueconf/dispatcher/dispatcher.py +126 -40
- trueconf/dispatcher/router.py +156 -38
- trueconf/exceptions.py +107 -10
- trueconf/fsm/__init__.py +20 -0
- trueconf/fsm/context.py +57 -0
- trueconf/fsm/filters.py +31 -0
- trueconf/fsm/key_builder.py +39 -0
- trueconf/fsm/manager.py +36 -0
- trueconf/fsm/middleware.py +24 -0
- trueconf/fsm/state.py +135 -0
- trueconf/fsm/storage/__init__.py +4 -0
- trueconf/fsm/storage/base.py +36 -0
- trueconf/fsm/storage/memory.py +57 -0
- trueconf/fsm/strategy.py +36 -0
- trueconf/middleware.py +78 -0
- trueconf/types/message.py +57 -21
- trueconf/utils/_ssl.py +44 -0
- trueconf/utils/_token.py +4 -4
- trueconf/utils/_version_checker.py +6 -0
- trueconf/utils/formatting/__init__.py +21 -0
- trueconf/utils/formatting/decorations.py +87 -0
- trueconf/utils/formatting/text.py +252 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/WHEEL +0 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/top_level.txt +0 -0
trueconf/fsm/context.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any, overload
|
|
5
|
+
|
|
6
|
+
from trueconf.fsm.key_builder import StorageKey
|
|
7
|
+
from trueconf.fsm.state import State
|
|
8
|
+
from trueconf.fsm.storage.base import BaseStorage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FSMContext:
|
|
12
|
+
def __init__(self, storage: BaseStorage, key: StorageKey) -> None:
|
|
13
|
+
self._storage = storage
|
|
14
|
+
self._key = key
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def key(self) -> StorageKey:
|
|
18
|
+
return self._key
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def storage(self) -> BaseStorage:
|
|
22
|
+
return self._storage
|
|
23
|
+
|
|
24
|
+
async def get_state(self) -> str | None:
|
|
25
|
+
return await self._storage.get_state(self._key)
|
|
26
|
+
|
|
27
|
+
async def set_state(self, state: State | str | None) -> None:
|
|
28
|
+
value = str(state) if isinstance(state, State) else state
|
|
29
|
+
await self._storage.set_state(self._key, value)
|
|
30
|
+
|
|
31
|
+
async def get_data(self) -> dict[str, Any]:
|
|
32
|
+
return await self._storage.get_data(self._key)
|
|
33
|
+
|
|
34
|
+
async def set_data(self, data: Mapping[str, Any]) -> None:
|
|
35
|
+
await self._storage.set_data(self._key, dict(data))
|
|
36
|
+
|
|
37
|
+
@overload
|
|
38
|
+
async def get_value(self, key: str) -> Any | None: ...
|
|
39
|
+
|
|
40
|
+
@overload
|
|
41
|
+
async def get_value(self, key: str, default: Any) -> Any: ...
|
|
42
|
+
|
|
43
|
+
async def get_value(self, key: str, default: Any | None = None) -> Any | None:
|
|
44
|
+
return await self._storage.get_value(self._key, key, default)
|
|
45
|
+
|
|
46
|
+
async def update_data(
|
|
47
|
+
self,
|
|
48
|
+
data: Mapping[str, Any] | None = None,
|
|
49
|
+
**kwargs: Any,
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
if data:
|
|
52
|
+
kwargs.update(data)
|
|
53
|
+
return await self._storage.update_data(self._key, kwargs)
|
|
54
|
+
|
|
55
|
+
async def clear(self) -> None:
|
|
56
|
+
await self.set_state(None)
|
|
57
|
+
await self.set_data({})
|
trueconf/fsm/filters.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from trueconf.fsm.context import FSMContext
|
|
6
|
+
from trueconf.fsm.state import State
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StateFilter:
|
|
10
|
+
def __init__(self, *states: State | str | None) -> None:
|
|
11
|
+
self._states: set[str | None] = {
|
|
12
|
+
str(s) if isinstance(s, State) else s for s in states
|
|
13
|
+
}
|
|
14
|
+
self._wildcard = "*" in self._states
|
|
15
|
+
|
|
16
|
+
async def __call__(
|
|
17
|
+
self,
|
|
18
|
+
event: Any,
|
|
19
|
+
*,
|
|
20
|
+
state: FSMContext | None = None,
|
|
21
|
+
) -> bool:
|
|
22
|
+
if state is None:
|
|
23
|
+
raise RuntimeError(
|
|
24
|
+
"StateFilter requires FSMContext in data['state'], but it was not found. "
|
|
25
|
+
"Make sure FSMMiddleware is registered as outer middleware "
|
|
26
|
+
"before any StateFilter is evaluated."
|
|
27
|
+
)
|
|
28
|
+
if self._wildcard:
|
|
29
|
+
return True
|
|
30
|
+
current = await state.get_state()
|
|
31
|
+
return current in self._states
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True)
|
|
8
|
+
class StorageKey:
|
|
9
|
+
bot_id: str | None
|
|
10
|
+
chat_id: str
|
|
11
|
+
user_id: str
|
|
12
|
+
destiny: str = "default"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class KeyBuilder(Protocol):
|
|
17
|
+
def build(self, bot: Any, event: Any) -> StorageKey: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DefaultKeyBuilder:
|
|
21
|
+
def build(self, bot: Any, event: Any) -> StorageKey:
|
|
22
|
+
bot_id = getattr(bot, "me_id", None) or getattr(bot, "id", None)
|
|
23
|
+
|
|
24
|
+
chat_id = (
|
|
25
|
+
getattr(event, "chat_id", None)
|
|
26
|
+
or getattr(getattr(event, "chat", None), "id", None)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
user = getattr(event, "from_user", None) or getattr(event, "author", None)
|
|
30
|
+
user_id = getattr(user, "id", None) if user else None
|
|
31
|
+
|
|
32
|
+
if chat_id is None or user_id is None:
|
|
33
|
+
raise RuntimeError(
|
|
34
|
+
f"Cannot build FSM StorageKey: event of type {type(event).__name__} "
|
|
35
|
+
f"has no chat_id ({chat_id}) or user_id ({user_id}). "
|
|
36
|
+
f"Provide a custom KeyBuilder to Dispatcher.setup_fsm()."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return StorageKey(bot_id=bot_id, chat_id=str(chat_id), user_id=str(user_id))
|
trueconf/fsm/manager.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from trueconf.fsm.context import FSMContext
|
|
6
|
+
from trueconf.fsm.key_builder import DefaultKeyBuilder, KeyBuilder, StorageKey
|
|
7
|
+
from trueconf.fsm.storage.base import BaseStorage
|
|
8
|
+
from trueconf.fsm.storage.memory import MemoryStorage
|
|
9
|
+
from trueconf.fsm.strategy import FSMStrategy, apply_strategy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FSMManager:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
storage: BaseStorage | None = None,
|
|
16
|
+
key_builder: KeyBuilder | None = None,
|
|
17
|
+
strategy: FSMStrategy = FSMStrategy.USER_IN_CHAT,
|
|
18
|
+
) -> None:
|
|
19
|
+
self.storage: BaseStorage = storage or MemoryStorage()
|
|
20
|
+
self.key_builder: KeyBuilder = key_builder or DefaultKeyBuilder()
|
|
21
|
+
self.strategy = strategy
|
|
22
|
+
|
|
23
|
+
def get_context(self, bot: Any, event: Any) -> FSMContext:
|
|
24
|
+
key = self.key_builder.build(bot, event)
|
|
25
|
+
chat_id, user_id = apply_strategy(
|
|
26
|
+
self.strategy,
|
|
27
|
+
key.chat_id,
|
|
28
|
+
key.user_id,
|
|
29
|
+
)
|
|
30
|
+
adjusted_key = StorageKey(
|
|
31
|
+
bot_id=key.bot_id,
|
|
32
|
+
chat_id=chat_id,
|
|
33
|
+
user_id=user_id,
|
|
34
|
+
destiny=key.destiny,
|
|
35
|
+
)
|
|
36
|
+
return FSMContext(self.storage, adjusted_key)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from trueconf.fsm.manager import FSMManager
|
|
6
|
+
from trueconf.middleware import BaseMiddleware
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FSMMiddleware(BaseMiddleware):
|
|
10
|
+
def __init__(self, fsm_manager: FSMManager) -> None:
|
|
11
|
+
self._manager = fsm_manager
|
|
12
|
+
|
|
13
|
+
async def __call__(
|
|
14
|
+
self,
|
|
15
|
+
handler: Any,
|
|
16
|
+
event: Any,
|
|
17
|
+
data: dict[str, Any],
|
|
18
|
+
) -> None:
|
|
19
|
+
bot = data.get("bot")
|
|
20
|
+
if bot is not None:
|
|
21
|
+
context = self._manager.get_context(bot, event)
|
|
22
|
+
data["state"] = context
|
|
23
|
+
data["raw_state"] = await context.get_state()
|
|
24
|
+
return await handler(event, data)
|
trueconf/fsm/state.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class State:
|
|
8
|
+
def __init__(self, state: str | None = None) -> None:
|
|
9
|
+
self._custom_state = state
|
|
10
|
+
self._name: str | None = None
|
|
11
|
+
self._group: type[StatesGroup] | None = None
|
|
12
|
+
|
|
13
|
+
def bind(self, group: type[StatesGroup], name: str) -> None:
|
|
14
|
+
if self._group is not None or self._name is not None:
|
|
15
|
+
raise RuntimeError(
|
|
16
|
+
f"State '{self._name}' is already bound to {self._group}. "
|
|
17
|
+
f"Cannot rebind to {group.__name__}:{name}."
|
|
18
|
+
)
|
|
19
|
+
self._group = group
|
|
20
|
+
self._name = name
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
if self._custom_state is not None:
|
|
24
|
+
return self._custom_state
|
|
25
|
+
if self._group is None or self._name is None:
|
|
26
|
+
raise RuntimeError(
|
|
27
|
+
"State is not bound to a StatesGroup. "
|
|
28
|
+
"Use State inside a StatesGroup class definition, "
|
|
29
|
+
"or pass a custom string: State('my:state')."
|
|
30
|
+
)
|
|
31
|
+
return f"{self._group.__full_group_name__}:{self._name}"
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
return f"State({str(self)!r})"
|
|
35
|
+
|
|
36
|
+
def __eq__(self, other: object) -> bool:
|
|
37
|
+
if isinstance(other, State):
|
|
38
|
+
return str(self) == str(other)
|
|
39
|
+
if isinstance(other, str):
|
|
40
|
+
return str(self) == other
|
|
41
|
+
return NotImplemented
|
|
42
|
+
|
|
43
|
+
def __hash__(self) -> int:
|
|
44
|
+
return hash(str(self))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _StatesGroupMeta(type):
|
|
48
|
+
__parent__: type[StatesGroup] | None
|
|
49
|
+
__childs__: tuple[type[StatesGroup], ...]
|
|
50
|
+
__states__: tuple[State, ...]
|
|
51
|
+
__all_states__: tuple[State, ...]
|
|
52
|
+
__all_childs__: tuple[type[StatesGroup], ...]
|
|
53
|
+
|
|
54
|
+
def __new__(
|
|
55
|
+
mcs,
|
|
56
|
+
name: str,
|
|
57
|
+
bases: tuple[type, ...],
|
|
58
|
+
namespace: dict[str, Any],
|
|
59
|
+
) -> _StatesGroupMeta:
|
|
60
|
+
cls = super().__new__(mcs, name, bases, namespace)
|
|
61
|
+
|
|
62
|
+
states: list[State] = []
|
|
63
|
+
childs: list[type[StatesGroup]] = []
|
|
64
|
+
|
|
65
|
+
for key, value in namespace.items():
|
|
66
|
+
if isinstance(value, State):
|
|
67
|
+
value.bind(cls, key)
|
|
68
|
+
states.append(value)
|
|
69
|
+
elif (
|
|
70
|
+
inspect.isclass(value)
|
|
71
|
+
and issubclass(value, StatesGroup)
|
|
72
|
+
and value is not StatesGroup
|
|
73
|
+
):
|
|
74
|
+
child = cls._prepare_child(value)
|
|
75
|
+
childs.append(child)
|
|
76
|
+
|
|
77
|
+
cls.__parent__ = None # type: ignore[attr-defined]
|
|
78
|
+
cls.__childs__ = tuple(childs) # type: ignore[attr-defined]
|
|
79
|
+
cls.__states__ = tuple(states) # type: ignore[attr-defined]
|
|
80
|
+
cls.__all_childs__ = cls._get_all_childs() # type: ignore[attr-defined]
|
|
81
|
+
cls.__all_states__ = cls._get_all_states() # type: ignore[attr-defined]
|
|
82
|
+
|
|
83
|
+
return cls
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def __full_group_name__(cls) -> str: # type: ignore[override]
|
|
87
|
+
if cls.__parent__ is not None:
|
|
88
|
+
return f"{cls.__parent__.__full_group_name__}.{cls.__name__}"
|
|
89
|
+
return cls.__name__
|
|
90
|
+
|
|
91
|
+
def _prepare_child(cls, child: type[StatesGroup]) -> type[StatesGroup]:
|
|
92
|
+
child.__parent__ = cls # type: ignore[assignment]
|
|
93
|
+
return child
|
|
94
|
+
|
|
95
|
+
def _get_all_childs(cls) -> tuple[type[StatesGroup], ...]:
|
|
96
|
+
result: list[type[StatesGroup]] = list(cls.__childs__)
|
|
97
|
+
for child in cls.__childs__:
|
|
98
|
+
result.extend(child.__childs__)
|
|
99
|
+
return tuple(result)
|
|
100
|
+
|
|
101
|
+
def _get_all_states(cls) -> tuple[State, ...]:
|
|
102
|
+
result: list[State] = list(cls.__states__)
|
|
103
|
+
for child in cls.__childs__:
|
|
104
|
+
result.extend(child.__all_states__)
|
|
105
|
+
return tuple(result)
|
|
106
|
+
|
|
107
|
+
def __contains__(cls, item: Any) -> bool:
|
|
108
|
+
if isinstance(item, str):
|
|
109
|
+
return item in tuple(str(s) for s in cls.__all_states__)
|
|
110
|
+
if isinstance(item, State):
|
|
111
|
+
return item in cls.__all_states__
|
|
112
|
+
if isinstance(item, _StatesGroupMeta):
|
|
113
|
+
return item in cls.__all_childs__
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def __iter__(cls) -> Any:
|
|
117
|
+
return iter(cls.__all_states__)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class StatesGroup(metaclass=_StatesGroupMeta):
|
|
121
|
+
__parent__: type[StatesGroup] | None = None
|
|
122
|
+
__childs__: tuple[type[StatesGroup], ...] = ()
|
|
123
|
+
__states__: tuple[State, ...] = ()
|
|
124
|
+
__all_states__: tuple[State, ...] = ()
|
|
125
|
+
__all_childs__: tuple[type[StatesGroup], ...] = ()
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def get_root(cls) -> type[StatesGroup]:
|
|
129
|
+
if cls.__parent__ is None:
|
|
130
|
+
return cls
|
|
131
|
+
return cls.__parent__.get_root()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
any_state = State(state="*")
|
|
135
|
+
default_state = State()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from trueconf.fsm.key_builder import StorageKey
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseStorage(ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
async def get_state(self, key: StorageKey) -> str | None: ...
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
async def set_state(self, key: StorageKey, state: str | None) -> None: ...
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
async def get_data(self, key: StorageKey) -> dict[str, Any]: ...
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
async def set_data(self, key: StorageKey, data: dict[str, Any]) -> None: ...
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
async def update_data(self, key: StorageKey, updates: dict[str, Any]) -> dict[str, Any]: ...
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def clear(self, key: StorageKey) -> None: ...
|
|
25
|
+
|
|
26
|
+
async def get_value(
|
|
27
|
+
self,
|
|
28
|
+
key: StorageKey,
|
|
29
|
+
dict_key: str,
|
|
30
|
+
default: Any | None = None,
|
|
31
|
+
) -> Any | None:
|
|
32
|
+
data = await self.get_data(key)
|
|
33
|
+
return data.get(dict_key, default)
|
|
34
|
+
|
|
35
|
+
async def close(self) -> None:
|
|
36
|
+
pass
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import copy
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from trueconf.fsm.key_builder import StorageKey
|
|
8
|
+
from trueconf.fsm.storage.base import BaseStorage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class _Record:
|
|
13
|
+
state: str | None = None
|
|
14
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MemoryStorage(BaseStorage):
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self._records: dict[StorageKey, _Record] = {}
|
|
20
|
+
|
|
21
|
+
def _get_or_create(self, key: StorageKey) -> _Record:
|
|
22
|
+
if key not in self._records:
|
|
23
|
+
self._records[key] = _Record()
|
|
24
|
+
return self._records[key]
|
|
25
|
+
|
|
26
|
+
async def get_state(self, key: StorageKey) -> str | None:
|
|
27
|
+
record = self._records.get(key)
|
|
28
|
+
return record.state if record else None
|
|
29
|
+
|
|
30
|
+
async def set_state(self, key: StorageKey, state: str | None) -> None:
|
|
31
|
+
self._get_or_create(key).state = state
|
|
32
|
+
|
|
33
|
+
async def get_data(self, key: StorageKey) -> dict[str, Any]:
|
|
34
|
+
record = self._records.get(key)
|
|
35
|
+
return record.data.copy() if record else {}
|
|
36
|
+
|
|
37
|
+
async def set_data(self, key: StorageKey, data: dict[str, Any]) -> None:
|
|
38
|
+
self._get_or_create(key).data = data.copy()
|
|
39
|
+
|
|
40
|
+
async def update_data(self, key: StorageKey, updates: dict[str, Any]) -> dict[str, Any]:
|
|
41
|
+
record = self._get_or_create(key)
|
|
42
|
+
record.data.update(updates)
|
|
43
|
+
return record.data.copy()
|
|
44
|
+
|
|
45
|
+
async def clear(self, key: StorageKey) -> None:
|
|
46
|
+
self._records.pop(key, None)
|
|
47
|
+
|
|
48
|
+
async def get_value(
|
|
49
|
+
self,
|
|
50
|
+
key: StorageKey,
|
|
51
|
+
dict_key: str,
|
|
52
|
+
default: Any | None = None,
|
|
53
|
+
) -> Any | None:
|
|
54
|
+
record = self._records.get(key)
|
|
55
|
+
if record is None:
|
|
56
|
+
return default
|
|
57
|
+
return copy(record.data.get(dict_key, default))
|
trueconf/fsm/strategy.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum, auto
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FSMStrategy(Enum):
|
|
7
|
+
"""FSM strategy for storage key generation.
|
|
8
|
+
|
|
9
|
+
Determines how chat_id and user_id are combined to form the storage key.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
USER_IN_CHAT = auto()
|
|
13
|
+
"""State is stored per user per chat. Default behavior."""
|
|
14
|
+
|
|
15
|
+
CHAT = auto()
|
|
16
|
+
"""State is stored per chat (all users in a chat share the same state)."""
|
|
17
|
+
|
|
18
|
+
GLOBAL_USER = auto()
|
|
19
|
+
"""State is stored per user globally (across all chats)."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def apply_strategy(
|
|
23
|
+
strategy: FSMStrategy,
|
|
24
|
+
chat_id: str,
|
|
25
|
+
user_id: str,
|
|
26
|
+
) -> tuple[str, str]:
|
|
27
|
+
"""Apply FSM strategy to chat_id and user_id.
|
|
28
|
+
|
|
29
|
+
Returns (effective_chat_id, effective_user_id) for StorageKey construction.
|
|
30
|
+
"""
|
|
31
|
+
if strategy == FSMStrategy.CHAT:
|
|
32
|
+
return chat_id, chat_id
|
|
33
|
+
if strategy == FSMStrategy.GLOBAL_USER:
|
|
34
|
+
return user_id, user_id
|
|
35
|
+
# USER_IN_CHAT (default)
|
|
36
|
+
return chat_id, user_id
|
trueconf/middleware.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict
|
|
4
|
+
|
|
5
|
+
from trueconf.filters.base import Event
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("chat_bot")
|
|
11
|
+
|
|
12
|
+
MiddlewareHandler = Callable[[Event, Dict[str, Any]], Awaitable[None]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseMiddleware:
|
|
16
|
+
"""
|
|
17
|
+
Base middleware class for event processing pipeline.
|
|
18
|
+
|
|
19
|
+
Middleware wraps event handlers and can intercept, modify, or block events
|
|
20
|
+
before they reach the actual handler. To block an event, simply do not call
|
|
21
|
+
``await handler(event, data)``.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> class LoggingMiddleware(BaseMiddleware):
|
|
25
|
+
... async def __call__(self, handler, event, data):
|
|
26
|
+
... logger.info(f"Event: {event}")
|
|
27
|
+
... await handler(event, data)
|
|
28
|
+
... logger.info("Done")
|
|
29
|
+
|
|
30
|
+
>>> router.outer_middleware(LoggingMiddleware())
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
async def __call__(
|
|
34
|
+
self,
|
|
35
|
+
handler: MiddlewareHandler,
|
|
36
|
+
event: Event,
|
|
37
|
+
data: Dict[str, Any],
|
|
38
|
+
) -> None:
|
|
39
|
+
await handler(event, data)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SkipSelfMessages(BaseMiddleware):
|
|
43
|
+
"""
|
|
44
|
+
Middleware that drops messages sent by the bot itself.
|
|
45
|
+
|
|
46
|
+
Prevents the bot from reacting to its own messages when multiple bot sessions
|
|
47
|
+
are running. If the event is a ``Message`` and its ``author.id`` matches
|
|
48
|
+
``bot.me_id``, the event is silently dropped.
|
|
49
|
+
|
|
50
|
+
Registration:
|
|
51
|
+
Automatically registered when ``Bot(skip_self_messages=True)`` (default).
|
|
52
|
+
Alternatively:
|
|
53
|
+
>>> from trueconf.middleware import SkipSelfMessages
|
|
54
|
+
>>> dp.outer_middleware(SkipSelfMessages())
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
async def __call__(
|
|
58
|
+
self,
|
|
59
|
+
handler: MiddlewareHandler,
|
|
60
|
+
event: Event,
|
|
61
|
+
data: Dict[str, Any],
|
|
62
|
+
) -> None:
|
|
63
|
+
bot = data.get("bot")
|
|
64
|
+
if bot is None:
|
|
65
|
+
await handler(event, data)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
from trueconf.types.message import Message
|
|
69
|
+
|
|
70
|
+
if isinstance(event, Message) and event.author.id == bot.me_id:
|
|
71
|
+
logger.debug(
|
|
72
|
+
"[SkipSelfMessages] dropped message %s from bot self (author=%s)",
|
|
73
|
+
event.message_id,
|
|
74
|
+
event.author.id,
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
await handler(event, data)
|