maxapi-python 2.4.1__py3-none-any.whl → 2.5.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.
- {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/METADATA +17 -9
- {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/RECORD +74 -45
- {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +5 -1
- pymax/_data/apk_fingerprints.json +272 -0
- pymax/api/auth/service.py +1 -0
- pymax/api/auth/types.py +3 -0
- pymax/api/bots/payloads.py +8 -0
- pymax/api/bots/service.py +13 -1
- pymax/api/chats/enums.py +1 -0
- pymax/api/chats/payloads.py +19 -0
- pymax/api/chats/service.py +34 -0
- pymax/api/facade.py +2 -0
- pymax/api/messages/enums.py +1 -0
- pymax/api/messages/payloads.py +47 -1
- pymax/api/messages/service.py +308 -24
- pymax/api/response.py +1 -1
- pymax/api/stories/__init__.py +2 -0
- pymax/api/stories/enums.py +19 -0
- pymax/api/stories/payloads.py +64 -0
- pymax/api/stories/service.py +198 -0
- pymax/api/uploads/payloads.py +10 -1
- pymax/api/uploads/service.py +94 -7
- pymax/app.py +16 -4
- pymax/auth/qr.py +11 -1
- pymax/base.py +70 -25
- pymax/client.py +0 -1
- pymax/config.py +38 -32
- pymax/di/__init__.py +2 -0
- pymax/di/context.py +18 -0
- pymax/di/resolver.py +38 -0
- pymax/dispatch/__init__.py +7 -0
- pymax/dispatch/dispatcher.py +143 -40
- pymax/dispatch/enums.py +1 -0
- pymax/dispatch/mapping.py +23 -3
- pymax/dispatch/middleware.py +32 -0
- pymax/dispatch/resolvers.py +4 -0
- pymax/dispatch/router.py +86 -34
- pymax/filters/__init__.py +15 -0
- pymax/filters/base.py +218 -0
- pymax/filters/containers/__init__.py +2 -0
- pymax/filters/containers/chat.py +240 -0
- pymax/filters/containers/message.py +147 -0
- pymax/filters/filters.py +211 -0
- pymax/filters/magic.py +13 -0
- pymax/filters/presets.py +88 -0
- pymax/filters/refs.py +56 -0
- pymax/formatting/markdown.py +55 -1
- pymax/fsm/__init__.py +3 -0
- pymax/fsm/context.py +30 -0
- pymax/fsm/protocol.py +14 -0
- pymax/fsm/state.py +70 -0
- pymax/fsm/storage.py +151 -0
- pymax/infra/base.py +2 -0
- pymax/infra/bots.py +20 -0
- pymax/infra/chat.py +28 -0
- pymax/infra/message.py +195 -6
- pymax/infra/stories.py +205 -0
- pymax/integrations/__init__.py +0 -0
- pymax/integrations/dishka.py +109 -0
- pymax/protocol/enums.py +2 -0
- pymax/protocol/tcp/payload.py +0 -2
- pymax/types/domain/__init__.py +23 -1
- pymax/types/domain/attachments/__init__.py +1 -1
- pymax/types/domain/attachments/keyboards/__init__.py +1 -1
- pymax/types/domain/attachments/keyboards/enums.py +12 -0
- pymax/types/domain/attachments/keyboards/inline.py +74 -3
- pymax/types/domain/callback.py +19 -0
- pymax/types/domain/comment.py +24 -0
- pymax/types/domain/message.py +91 -22
- pymax/types/domain/story.py +126 -0
- pymax/types/events/reaction.py +28 -4
- pymax/versions/catalog.py +1 -1
- {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/licenses/LICENSE +0 -0
pymax/filters/refs.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
|
|
5
|
+
from .base import MISSING, ClientT, FieldRef
|
|
6
|
+
from .filters import (
|
|
7
|
+
BetweenFilter,
|
|
8
|
+
ContainsFilter,
|
|
9
|
+
EndsWithFilter,
|
|
10
|
+
GreaterThanFilter,
|
|
11
|
+
GreaterThanOrEqualFilter,
|
|
12
|
+
LessThanFilter,
|
|
13
|
+
LessThanOrEqualFilter,
|
|
14
|
+
StartsWithFilter,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
EventT = TypeVar("EventT")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TextFieldRef(FieldRef[EventT, ClientT]):
|
|
21
|
+
def contains(self, text: str, *, ignore_case: bool = False) -> ContainsFilter:
|
|
22
|
+
return ContainsFilter(self.path, self.expected_type, text, ignore_case)
|
|
23
|
+
|
|
24
|
+
def startswith(self, text: str, *, ignore_case: bool = False) -> StartsWithFilter:
|
|
25
|
+
return StartsWithFilter(self.path, self.expected_type, text, ignore_case)
|
|
26
|
+
|
|
27
|
+
def endswith(self, text: str, *, ignore_case: bool = False) -> EndsWithFilter:
|
|
28
|
+
return EndsWithFilter(self.path, self.expected_type, text, ignore_case)
|
|
29
|
+
|
|
30
|
+
def __call__(self, event: EventT, _: ClientT) -> bool:
|
|
31
|
+
if not isinstance(event, self.expected_type):
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
value = self.resolve_path(event)
|
|
35
|
+
|
|
36
|
+
if value is MISSING:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
return bool(value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NumberFieldRef(FieldRef[EventT, ClientT]):
|
|
43
|
+
def __lt__(self, value: float) -> LessThanFilter:
|
|
44
|
+
return LessThanFilter(self.path, self.expected_type, value)
|
|
45
|
+
|
|
46
|
+
def __le__(self, value: float) -> LessThanOrEqualFilter:
|
|
47
|
+
return LessThanOrEqualFilter(self.path, self.expected_type, value)
|
|
48
|
+
|
|
49
|
+
def __gt__(self, value: float) -> GreaterThanFilter:
|
|
50
|
+
return GreaterThanFilter(self.path, self.expected_type, value)
|
|
51
|
+
|
|
52
|
+
def __ge__(self, value: float) -> GreaterThanOrEqualFilter:
|
|
53
|
+
return GreaterThanOrEqualFilter(self.path, self.expected_type, value)
|
|
54
|
+
|
|
55
|
+
def between(self, minimum: float, maximum: float) -> BetweenFilter:
|
|
56
|
+
return BetweenFilter(self.path, self.expected_type, minimum, maximum)
|
pymax/formatting/markdown.py
CHANGED
|
@@ -51,6 +51,29 @@ class Formatter:
|
|
|
51
51
|
|
|
52
52
|
return label, url, url_end + 1
|
|
53
53
|
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _is_intraword_single_marker(text: str, i: int, marker: str) -> bool:
|
|
56
|
+
if marker != "_":
|
|
57
|
+
return False
|
|
58
|
+
if i == 0 or i + 1 >= len(text):
|
|
59
|
+
return False
|
|
60
|
+
return text[i - 1].isalnum() and text[i + 1].isalnum()
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _find_closing_marker(
|
|
64
|
+
text: str,
|
|
65
|
+
marker: str,
|
|
66
|
+
start: int,
|
|
67
|
+
end: int | None = None,
|
|
68
|
+
) -> int:
|
|
69
|
+
while True:
|
|
70
|
+
closing_index = text.find(marker, start, len(text) if end is None else end)
|
|
71
|
+
if closing_index == -1:
|
|
72
|
+
return -1
|
|
73
|
+
if not Formatter._is_intraword_single_marker(text, closing_index, marker):
|
|
74
|
+
return closing_index
|
|
75
|
+
start = closing_index + len(marker)
|
|
76
|
+
|
|
54
77
|
@staticmethod
|
|
55
78
|
def format_markdown(text: str) -> tuple[str, list[Element]]:
|
|
56
79
|
clean_text = ""
|
|
@@ -65,6 +88,34 @@ class Formatter:
|
|
|
65
88
|
while i < len(text):
|
|
66
89
|
handled = False
|
|
67
90
|
|
|
91
|
+
for marker in ("```", "`"):
|
|
92
|
+
if marker not in active:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
closing_index = text.find(marker, i)
|
|
96
|
+
content = text[i:closing_index]
|
|
97
|
+
clean_text += content
|
|
98
|
+
clean_pos += Formatter._code_units_len(content)
|
|
99
|
+
|
|
100
|
+
start = active.pop(marker)
|
|
101
|
+
length = clean_pos - start
|
|
102
|
+
if length > 0:
|
|
103
|
+
entities.append(
|
|
104
|
+
Element(
|
|
105
|
+
type=Formatter.MARKERS[marker],
|
|
106
|
+
from_=start,
|
|
107
|
+
length=length,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
i = closing_index + len(marker)
|
|
112
|
+
handled = True
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
if handled:
|
|
116
|
+
line_start = False
|
|
117
|
+
continue
|
|
118
|
+
|
|
68
119
|
# LINK: [text](url)
|
|
69
120
|
parsed_link = Formatter._parse_link(text, i, clean_pos)
|
|
70
121
|
|
|
@@ -155,6 +206,8 @@ class Formatter:
|
|
|
155
206
|
for marker in Formatter.MARKER_ORDER:
|
|
156
207
|
if not text.startswith(marker, i):
|
|
157
208
|
continue
|
|
209
|
+
if Formatter._is_intraword_single_marker(text, i, marker):
|
|
210
|
+
continue
|
|
158
211
|
|
|
159
212
|
marker_len = len(marker)
|
|
160
213
|
|
|
@@ -180,7 +233,8 @@ class Formatter:
|
|
|
180
233
|
break
|
|
181
234
|
|
|
182
235
|
end = text.find("\n", i + marker_len)
|
|
183
|
-
closing_index =
|
|
236
|
+
closing_index = Formatter._find_closing_marker(
|
|
237
|
+
text,
|
|
184
238
|
marker,
|
|
185
239
|
i + marker_len,
|
|
186
240
|
None if end == -1 else end,
|
pymax/fsm/__init__.py
ADDED
pymax/fsm/context.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from .protocol import FSMStorage
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class FSMKey:
|
|
8
|
+
client_id: int
|
|
9
|
+
user_id: int
|
|
10
|
+
chat_id: int
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FSMContext:
|
|
14
|
+
"""Контекст состояния пользователя в текущем чате.
|
|
15
|
+
|
|
16
|
+
PyMax передает его в обязательный параметр обработчика сообщения
|
|
17
|
+
с аннотацией ``FSMContext``. Состояние и данные доступны через ``storage``.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
storage: Хранилище состояния и данных текущего контекста.
|
|
21
|
+
key: Ключ контекста для текущих клиента, пользователя и чата.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
storage: FSMStorage,
|
|
27
|
+
key: FSMKey,
|
|
28
|
+
) -> None:
|
|
29
|
+
self.storage = storage
|
|
30
|
+
self.key = key
|
pymax/fsm/protocol.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Any, Protocol
|
|
2
|
+
|
|
3
|
+
from .state import State
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FSMStorage(Protocol):
|
|
7
|
+
async def get_state(self) -> str | None: ...
|
|
8
|
+
async def set_state(self, state: str | State | None, ttl: int | None = None) -> None: ...
|
|
9
|
+
|
|
10
|
+
async def get_data(self) -> dict[str, Any]: ...
|
|
11
|
+
async def set_data(self, key: str, value: Any) -> None: ...
|
|
12
|
+
async def update_data(self, **data: Any) -> None: ...
|
|
13
|
+
|
|
14
|
+
async def clear(self) -> None: ...
|
pymax/fsm/state.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from pymax.base import BaseClient
|
|
7
|
+
|
|
8
|
+
from pymax.filters import Filter
|
|
9
|
+
from pymax.types import Message
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class State(Filter[Message, "BaseClient"]):
|
|
13
|
+
"""Состояние FSM и фильтр сообщений по этому состоянию.
|
|
14
|
+
|
|
15
|
+
Объявляйте экземпляры атрибутами ``FsmGroup``. Один и тот же экземпляр
|
|
16
|
+
можно передать в ``ctx.storage.set_state()`` и ``router.on_message()``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
owner: type
|
|
20
|
+
name: str
|
|
21
|
+
|
|
22
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
23
|
+
self.owner = owner
|
|
24
|
+
self.name = name
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def full_name(self) -> str:
|
|
28
|
+
"""Возвращает полное имя состояния из имени модуля, группы и атрибута."""
|
|
29
|
+
return f"{self.owner.__module__}.{self.owner.__qualname__}:{self.name}"
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return self.full_name
|
|
33
|
+
|
|
34
|
+
async def __call__(
|
|
35
|
+
self,
|
|
36
|
+
event: Message,
|
|
37
|
+
client: BaseClient,
|
|
38
|
+
) -> bool:
|
|
39
|
+
if event.chat_id is None or event.sender is None:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
fsm_key = client._app.fsm_storage.resolve_key(client, event)
|
|
43
|
+
|
|
44
|
+
if not fsm_key:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
record = client._app.fsm_storage.records.get(fsm_key)
|
|
48
|
+
|
|
49
|
+
if record is None:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
return await record.get_state() == self.full_name
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FsmGroup:
|
|
56
|
+
"""Группа состояний, объявленных атрибутами класса через ``State()``."""
|
|
57
|
+
|
|
58
|
+
__states__: dict[str, State] = {}
|
|
59
|
+
|
|
60
|
+
def __init_subclass__(cls) -> None:
|
|
61
|
+
super().__init_subclass__()
|
|
62
|
+
|
|
63
|
+
inherited: dict[str, State] = {}
|
|
64
|
+
|
|
65
|
+
for base in cls.__bases__:
|
|
66
|
+
inherited.update(getattr(base, "__states__", {}))
|
|
67
|
+
|
|
68
|
+
own = {name: value for name, value in vars(cls).items() if isinstance(value, State)}
|
|
69
|
+
|
|
70
|
+
cls.__states__ = inherited | own
|
pymax/fsm/storage.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import TYPE_CHECKING, Any, AsyncGenerator
|
|
8
|
+
|
|
9
|
+
from pymax.fsm.context import FSMContext, FSMKey
|
|
10
|
+
from pymax.types.domain.message import Message
|
|
11
|
+
|
|
12
|
+
from .protocol import FSMStorage
|
|
13
|
+
from .state import State
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pymax.base import BaseClient
|
|
17
|
+
from pymax.dispatch import DispatchEvent
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InMemoryFSMStorage:
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self.records: dict[FSMKey, InMemoryFSM] = {}
|
|
23
|
+
self.locks: dict[FSMKey, asyncio.Lock] = {}
|
|
24
|
+
|
|
25
|
+
def resolve_key(
|
|
26
|
+
self,
|
|
27
|
+
client: BaseClient,
|
|
28
|
+
event: DispatchEvent,
|
|
29
|
+
) -> FSMKey | None:
|
|
30
|
+
if not isinstance(event, Message):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
if event.chat_id is None or event.sender is None:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
return FSMKey(
|
|
37
|
+
client_id=hash(client),
|
|
38
|
+
user_id=event.sender,
|
|
39
|
+
chat_id=event.chat_id,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def get_context(
|
|
43
|
+
self,
|
|
44
|
+
client: BaseClient,
|
|
45
|
+
event: Message,
|
|
46
|
+
) -> FSMContext | None:
|
|
47
|
+
if not event.chat_id or not event.sender:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
key = self.resolve_key(client, event)
|
|
51
|
+
|
|
52
|
+
if not key:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
record = self.records.get(key)
|
|
56
|
+
if record is None:
|
|
57
|
+
record = InMemoryFSM()
|
|
58
|
+
self.records[key] = record
|
|
59
|
+
|
|
60
|
+
return FSMContext(storage=record, key=key)
|
|
61
|
+
|
|
62
|
+
@asynccontextmanager
|
|
63
|
+
async def isolate(
|
|
64
|
+
self,
|
|
65
|
+
client: BaseClient,
|
|
66
|
+
event: DispatchEvent,
|
|
67
|
+
) -> AsyncGenerator[None]:
|
|
68
|
+
key = self.resolve_key(client, event)
|
|
69
|
+
|
|
70
|
+
if key is None:
|
|
71
|
+
yield
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
lock = self.locks.setdefault(key, asyncio.Lock())
|
|
75
|
+
|
|
76
|
+
async with lock:
|
|
77
|
+
yield
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class FsmState:
|
|
82
|
+
value: str | None = None
|
|
83
|
+
created_at: datetime = datetime.now()
|
|
84
|
+
ttl: int | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class InMemoryFSM(FSMStorage):
|
|
88
|
+
"""Состояние и данные одного FSM-контекста в памяти процесса.
|
|
89
|
+
|
|
90
|
+
В обработчике это хранилище доступно как ``ctx.storage``.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
self.state: FsmState = FsmState()
|
|
95
|
+
self.data: dict[str, Any] = {}
|
|
96
|
+
self.lock = asyncio.Lock()
|
|
97
|
+
|
|
98
|
+
async def get_state(self) -> str | None:
|
|
99
|
+
"""Возвращает имя состояния или ``None``, если оно не задано или истек TTL.
|
|
100
|
+
|
|
101
|
+
Истечение TTL сбрасывает только состояние, сохраняя данные контекста.
|
|
102
|
+
"""
|
|
103
|
+
async with self.lock:
|
|
104
|
+
if self.state.ttl is None:
|
|
105
|
+
return self.state.value
|
|
106
|
+
|
|
107
|
+
expires_at = self.state.created_at.timestamp() + self.state.ttl
|
|
108
|
+
|
|
109
|
+
if datetime.now().timestamp() >= expires_at:
|
|
110
|
+
self.state = FsmState()
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
return self.state.value
|
|
114
|
+
|
|
115
|
+
async def set_state(self, state: str | State | None, ttl: int | None = None) -> None:
|
|
116
|
+
"""Устанавливает состояние, сохраняя данные контекста.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
state: Состояние, его строковое имя или ``None`` для сброса.
|
|
120
|
+
ttl: Срок действия в секундах с момента установки.
|
|
121
|
+
``None`` означает отсутствие срока действия.
|
|
122
|
+
"""
|
|
123
|
+
async with self.lock:
|
|
124
|
+
if state is None:
|
|
125
|
+
self.state = FsmState()
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
self.state.value = str(state)
|
|
129
|
+
self.state.ttl = ttl
|
|
130
|
+
self.state.created_at = datetime.now()
|
|
131
|
+
|
|
132
|
+
async def get_data(self) -> dict[str, Any]:
|
|
133
|
+
"""Возвращает поверхностную копию словаря данных контекста."""
|
|
134
|
+
async with self.lock:
|
|
135
|
+
return self.data.copy()
|
|
136
|
+
|
|
137
|
+
async def set_data(self, key: str, value: Any) -> None:
|
|
138
|
+
"""Записывает одно значение, сохраняя остальные ключи данных."""
|
|
139
|
+
async with self.lock:
|
|
140
|
+
self.data[key] = value
|
|
141
|
+
|
|
142
|
+
async def update_data(self, **data: Any) -> None:
|
|
143
|
+
"""Добавляет или перезаписывает переданные ключи данных."""
|
|
144
|
+
async with self.lock:
|
|
145
|
+
self.data.update(data)
|
|
146
|
+
|
|
147
|
+
async def clear(self) -> None:
|
|
148
|
+
"""Сбрасывает состояние и удаляет все данные текущего контекста."""
|
|
149
|
+
async with self.lock:
|
|
150
|
+
self.state = FsmState()
|
|
151
|
+
self.data.clear()
|
pymax/infra/base.py
CHANGED
|
@@ -3,6 +3,7 @@ from .bots import BotsMixin
|
|
|
3
3
|
from .chat import ChatMixin
|
|
4
4
|
from .message import MessageMixin
|
|
5
5
|
from .self import SelfMixin
|
|
6
|
+
from .stories import StoriesMixin
|
|
6
7
|
from .user import UserMixin
|
|
7
8
|
|
|
8
9
|
|
|
@@ -13,5 +14,6 @@ class BaseMixin(
|
|
|
13
14
|
MessageMixin,
|
|
14
15
|
BotsMixin,
|
|
15
16
|
AuthMixin,
|
|
17
|
+
StoriesMixin,
|
|
16
18
|
):
|
|
17
19
|
"""Собирает публичные API-методы клиента."""
|
pymax/infra/bots.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from pymax.types import ButtonType, CallbackResponse
|
|
1
2
|
from pymax.types.domain import InitData
|
|
2
3
|
|
|
3
4
|
from .protocol import IClientProtocol
|
|
@@ -31,3 +32,22 @@ class BotsMixin(IClientProtocol):
|
|
|
31
32
|
chat_id=chat_id,
|
|
32
33
|
start_param=start_param,
|
|
33
34
|
)
|
|
35
|
+
|
|
36
|
+
async def send_callback(
|
|
37
|
+
self,
|
|
38
|
+
callback_id: str,
|
|
39
|
+
type: ButtonType,
|
|
40
|
+
payload: str | None = None,
|
|
41
|
+
) -> CallbackResponse:
|
|
42
|
+
"""Передает нажатие inline-кнопки бота.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
callback_id: ID из ``InlineKeyboardAttachment.callback_id``.
|
|
46
|
+
type: Тип нажатой кнопки.
|
|
47
|
+
payload: Данные кнопки, если они есть.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Результат нажатия. ``message`` и ``chat`` содержатся в нем,
|
|
51
|
+
только если Max вернул эти объекты.
|
|
52
|
+
"""
|
|
53
|
+
return await self._app.api.bots.send_callback(callback_id, type, payload)
|
pymax/infra/chat.py
CHANGED
|
@@ -414,3 +414,31 @@ class ChatMixin(IClientProtocol):
|
|
|
414
414
|
permissions: Непустой список прав администратора.
|
|
415
415
|
"""
|
|
416
416
|
return await self._app.api.chats.add_admin(chat_id, user_id, permissions)
|
|
417
|
+
|
|
418
|
+
async def set_channel_comments(self, chat_id: int, enabled: bool) -> None:
|
|
419
|
+
"""Включает или выключает комментарии канала."""
|
|
420
|
+
return await self._app.api.chats.set_channel_comments(
|
|
421
|
+
chat_id=chat_id,
|
|
422
|
+
enabled=enabled,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
async def block_comment_author(
|
|
426
|
+
self,
|
|
427
|
+
chat_id: int,
|
|
428
|
+
post_id: int,
|
|
429
|
+
user_ids: list[int],
|
|
430
|
+
message_id: int,
|
|
431
|
+
clean_msg_period: int = 0,
|
|
432
|
+
) -> bool:
|
|
433
|
+
"""Блокирует авторов в комментариях.
|
|
434
|
+
|
|
435
|
+
``message_id`` — ID комментария для модерации.
|
|
436
|
+
``clean_msg_period=0`` сохраняет комментарии, ``-1`` также удаляет их.
|
|
437
|
+
"""
|
|
438
|
+
return await self._app.api.chats.block_comment_author(
|
|
439
|
+
chat_id=chat_id,
|
|
440
|
+
post_id=post_id,
|
|
441
|
+
user_ids=user_ids,
|
|
442
|
+
message_id=message_id,
|
|
443
|
+
clean_msg_period=clean_msg_period,
|
|
444
|
+
)
|