maxapi-python 2.4.0__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.0.dist-info → maxapi_python-2.5.0.dist-info}/METADATA +22 -12
- maxapi_python-2.5.0.dist-info/RECORD +195 -0
- {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +8 -2
- pymax/_data/apk_fingerprints.json +374 -17
- pymax/_data/rootca_ssl_rsa2022.crt +33 -0
- pymax/api/auth/service.py +15 -2
- 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 +21 -1
- pymax/api/chats/service.py +41 -0
- pymax/api/facade.py +2 -0
- pymax/api/messages/__init__.py +1 -1
- pymax/api/messages/enums.py +1 -0
- pymax/api/messages/payloads.py +56 -4
- pymax/api/messages/service.py +405 -27
- 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 +42 -1
- pymax/api/uploads/service.py +171 -69
- pymax/app.py +39 -8
- pymax/auth/__init__.py +2 -0
- pymax/auth/exceptions.py +9 -0
- pymax/auth/providers.py +6 -2
- pymax/auth/qr.py +11 -1
- pymax/auth/sms.py +11 -1
- pymax/base.py +146 -27
- pymax/client.py +39 -11
- pymax/client_web.py +15 -12
- pymax/config.py +101 -53
- 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/files/base.py +58 -1
- pymax/files/video.py +32 -40
- pymax/files/voice.py +18 -4
- 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/fingerprint/fingerprint.py +5 -22
- pymax/fingerprint/models.py +1 -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 +37 -5
- pymax/infra/message.py +204 -9
- pymax/infra/protocol.py +5 -1
- 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/session/__init__.py +1 -1
- pymax/session/models.py +30 -0
- pymax/session/protocol.py +33 -7
- pymax/session/store.py +73 -5
- pymax/transport/tcp.py +11 -3
- pymax/types/domain/__init__.py +23 -1
- pymax/types/domain/attachments/__init__.py +1 -1
- pymax/types/domain/attachments/call.py +7 -1
- pymax/types/domain/attachments/contact.py +3 -3
- pymax/types/domain/attachments/control.py +3 -0
- pymax/types/domain/attachments/enums.py +16 -0
- 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/attachments/video.py +3 -3
- pymax/types/domain/callback.py +19 -0
- pymax/types/domain/comment.py +24 -0
- pymax/types/domain/enums.py +7 -0
- pymax/types/domain/message.py +158 -9
- pymax/types/domain/story.py +126 -0
- pymax/types/events/reaction.py +28 -4
- pymax/versions/__init__.py +2 -0
- pymax/versions/catalog.py +104 -0
- pymax/versions/exceptions.py +10 -0
- maxapi_python-2.4.0.dist-info/RECORD +0 -161
- {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/licenses/LICENSE +0 -0
pymax/filters/filters.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
4
|
+
|
|
5
|
+
from .base import ClientT, Filter, PathResolver
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
EventT = TypeVar("EventT")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContainsFilter(Filter[EventT, ClientT], PathResolver):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
path: list[str],
|
|
17
|
+
expected_type: type[EventT],
|
|
18
|
+
text: str,
|
|
19
|
+
ignore_case: bool,
|
|
20
|
+
) -> None:
|
|
21
|
+
self.path = path
|
|
22
|
+
self.expected_type = expected_type
|
|
23
|
+
self.text = text
|
|
24
|
+
self.ignore_case = ignore_case
|
|
25
|
+
|
|
26
|
+
def __call__(self, event: EventT, _: ClientT) -> bool:
|
|
27
|
+
if not isinstance(event, self.expected_type):
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
value = self.resolve_path(event)
|
|
31
|
+
|
|
32
|
+
if not isinstance(value, str):
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
if self.ignore_case:
|
|
36
|
+
return self.text.casefold() in value.casefold()
|
|
37
|
+
|
|
38
|
+
return self.text in value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class StartsWithFilter(Filter[EventT, ClientT], PathResolver):
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
path: list[str],
|
|
45
|
+
expected_type: type[EventT],
|
|
46
|
+
text: str,
|
|
47
|
+
ignore_case: bool,
|
|
48
|
+
) -> None:
|
|
49
|
+
self.path = path
|
|
50
|
+
self.expected_type = expected_type
|
|
51
|
+
self.text = text
|
|
52
|
+
self.ignore_case = ignore_case
|
|
53
|
+
|
|
54
|
+
def __call__(self, event: EventT, _: ClientT) -> bool:
|
|
55
|
+
if not isinstance(event, self.expected_type):
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
value = self.resolve_path(event)
|
|
59
|
+
|
|
60
|
+
if not isinstance(value, str):
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
if self.ignore_case:
|
|
64
|
+
return value.casefold().startswith(self.text.casefold())
|
|
65
|
+
|
|
66
|
+
return value.startswith(self.text)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EndsWithFilter(Filter[EventT, ClientT], PathResolver):
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
path: list[str],
|
|
73
|
+
expected_type: type[EventT],
|
|
74
|
+
text: str,
|
|
75
|
+
ignore_case: bool,
|
|
76
|
+
) -> None:
|
|
77
|
+
self.path = path
|
|
78
|
+
self.expected_type = expected_type
|
|
79
|
+
self.text = text
|
|
80
|
+
self.ignore_case = ignore_case
|
|
81
|
+
|
|
82
|
+
def __call__(self, event: EventT, _: ClientT) -> bool:
|
|
83
|
+
if not isinstance(event, self.expected_type):
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
value = self.resolve_path(event)
|
|
87
|
+
|
|
88
|
+
if not isinstance(value, str):
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
if self.ignore_case:
|
|
92
|
+
return value.casefold().endswith(self.text.casefold())
|
|
93
|
+
|
|
94
|
+
return value.endswith(self.text)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class LessThanFilter(Filter[EventT, ClientT], PathResolver):
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
path: list[str],
|
|
101
|
+
expected_type: type[EventT],
|
|
102
|
+
expected: float,
|
|
103
|
+
) -> None:
|
|
104
|
+
self.path = path
|
|
105
|
+
self.expected_type = expected_type
|
|
106
|
+
self.expected = expected
|
|
107
|
+
|
|
108
|
+
def __call__(self, event: EventT, _: ClientT) -> bool:
|
|
109
|
+
if not isinstance(event, self.expected_type):
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
value = self.resolve_path(event)
|
|
113
|
+
|
|
114
|
+
if not isinstance(value, int | float) or isinstance(value, bool):
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
return value < self.expected
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class LessThanOrEqualFilter(Filter[EventT, ClientT], PathResolver):
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
path: list[str],
|
|
124
|
+
expected_type: type[EventT],
|
|
125
|
+
expected: float,
|
|
126
|
+
) -> None:
|
|
127
|
+
self.path = path
|
|
128
|
+
self.expected_type = expected_type
|
|
129
|
+
self.expected = expected
|
|
130
|
+
|
|
131
|
+
def __call__(self, event: EventT, _: ClientT) -> bool:
|
|
132
|
+
if not isinstance(event, self.expected_type):
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
value = self.resolve_path(event)
|
|
136
|
+
|
|
137
|
+
if not isinstance(value, int | float) or isinstance(value, bool):
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
return value <= self.expected
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class GreaterThanFilter(Filter[EventT, ClientT], PathResolver):
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
path: list[str],
|
|
147
|
+
expected_type: type[EventT],
|
|
148
|
+
expected: float,
|
|
149
|
+
) -> None:
|
|
150
|
+
self.path = path
|
|
151
|
+
self.expected_type = expected_type
|
|
152
|
+
self.expected = expected
|
|
153
|
+
|
|
154
|
+
def __call__(self, event: EventT, _: ClientT) -> bool:
|
|
155
|
+
if not isinstance(event, self.expected_type):
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
value = self.resolve_path(event)
|
|
159
|
+
|
|
160
|
+
if not isinstance(value, int | float) or isinstance(value, bool):
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
return value > self.expected
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class GreaterThanOrEqualFilter(Filter[EventT, ClientT], PathResolver):
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
path: list[str],
|
|
170
|
+
expected_type: type[EventT],
|
|
171
|
+
expected: float,
|
|
172
|
+
) -> None:
|
|
173
|
+
self.path = path
|
|
174
|
+
self.expected_type = expected_type
|
|
175
|
+
self.expected = expected
|
|
176
|
+
|
|
177
|
+
def __call__(self, event: EventT, _: ClientT) -> bool:
|
|
178
|
+
if not isinstance(event, self.expected_type):
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
value = self.resolve_path(event)
|
|
182
|
+
|
|
183
|
+
if not isinstance(value, int | float) or isinstance(value, bool):
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
return value >= self.expected
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class BetweenFilter(Filter[EventT, ClientT], PathResolver):
|
|
190
|
+
def __init__(
|
|
191
|
+
self,
|
|
192
|
+
path: list[str],
|
|
193
|
+
expected_type: type[EventT],
|
|
194
|
+
minimum: float,
|
|
195
|
+
maximum: float,
|
|
196
|
+
) -> None:
|
|
197
|
+
self.path = path
|
|
198
|
+
self.expected_type = expected_type
|
|
199
|
+
self.minimum = minimum
|
|
200
|
+
self.maximum = maximum
|
|
201
|
+
|
|
202
|
+
def __call__(self, event: EventT, _: ClientT) -> bool:
|
|
203
|
+
if not isinstance(event, self.expected_type):
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
value = self.resolve_path(event)
|
|
207
|
+
|
|
208
|
+
if not isinstance(value, int | float) or isinstance(value, bool):
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
return self.minimum <= value <= self.maximum
|
pymax/filters/magic.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pymax.types import Chat, Message
|
|
2
|
+
|
|
3
|
+
from .containers import ChatContainer, MessageContainer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MagicFilter:
|
|
7
|
+
@property
|
|
8
|
+
def message(self) -> MessageContainer:
|
|
9
|
+
return MessageContainer(Message)
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def chat(self) -> ChatContainer:
|
|
13
|
+
return ChatContainer(Chat)
|
pymax/filters/presets.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from pymax.types import LinkType, Message
|
|
6
|
+
|
|
7
|
+
from .base import Filter
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pymax.base import BaseClient
|
|
11
|
+
from pymax.types.domain.message import Attachment
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Command(Filter[Message, "BaseClient"]):
|
|
15
|
+
"""Проверяет команду в первом слове текста сообщения.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
command: Имя команды, например ``"start"`` для ``/start``.
|
|
19
|
+
prefix: Префикс команды или кортеж допустимых префиксов.
|
|
20
|
+
|
|
21
|
+
Аргументы после команды не влияют на совпадение.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, command: str, prefix: str | tuple[str, ...] = "/") -> None:
|
|
25
|
+
self.prefixes = (prefix,) if isinstance(prefix, str) else prefix
|
|
26
|
+
|
|
27
|
+
for prefix_ in self.prefixes:
|
|
28
|
+
self.command = command.removeprefix(prefix_)
|
|
29
|
+
|
|
30
|
+
def __call__(self, message: Message, _: BaseClient) -> bool:
|
|
31
|
+
if not message.text:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
parts = message.text.split(maxsplit=1)
|
|
35
|
+
if not parts:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
dirty_command = parts[0]
|
|
39
|
+
|
|
40
|
+
for prefix in self.prefixes:
|
|
41
|
+
if message.text.startswith(prefix) and dirty_command[len(prefix) :] == self.command:
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Reply(Filter[Message, "BaseClient"]):
|
|
48
|
+
"""Проверяет, является ли сообщение ответом на другое сообщение."""
|
|
49
|
+
|
|
50
|
+
def __call__(self, message: Message, _: BaseClient) -> bool:
|
|
51
|
+
if not message.link:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
return message.link.type == LinkType.REPLY
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Forwarded(Filter[Message, "BaseClient"]):
|
|
58
|
+
"""Проверяет, является ли сообщение пересланным."""
|
|
59
|
+
|
|
60
|
+
def __call__(self, message: Message, _: BaseClient) -> bool:
|
|
61
|
+
if not message.link:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
return message.link.type == LinkType.FORWARD
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FromMe(Filter[Message, "BaseClient"]):
|
|
68
|
+
"""Проверяет, отправлено ли сообщение текущим пользователем клиента."""
|
|
69
|
+
|
|
70
|
+
def __call__(self, message: Message, client: BaseClient) -> bool:
|
|
71
|
+
if not message.sender or not client.me:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
return message.sender == client.me.contact.id
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class HasAttachment(Filter[Message, "BaseClient"]):
|
|
78
|
+
"""Проверяет наличие хотя бы одного вложения указанных типов.
|
|
79
|
+
|
|
80
|
+
Передавайте классы входящих вложений из ``pymax.types``, например
|
|
81
|
+
``PhotoAttachment``, а не классы файлов для отправки.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, *attachment_types: type[Attachment]) -> None:
|
|
85
|
+
self.expected_types = attachment_types
|
|
86
|
+
|
|
87
|
+
def __call__(self, message: Message, _: BaseClient) -> bool:
|
|
88
|
+
return any(isinstance(attach, self.expected_types) for attach in message.attaches)
|
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/fingerprint/fingerprint.py
CHANGED
|
@@ -1,47 +1,30 @@
|
|
|
1
1
|
import hashlib
|
|
2
|
-
import json
|
|
3
2
|
import struct
|
|
4
|
-
from importlib import resources
|
|
5
|
-
from typing import Any
|
|
6
3
|
|
|
7
4
|
from .models import ApkBuildFingerprint
|
|
8
5
|
|
|
9
6
|
|
|
10
7
|
class FingerprintGenerator:
|
|
11
|
-
def __init__(
|
|
12
|
-
self
|
|
13
|
-
) -> None:
|
|
14
|
-
self.path = resources.files("pymax._data") / "apk_fingerprints.json"
|
|
15
|
-
self.data = self.load_fingerprints()
|
|
16
|
-
|
|
17
|
-
def load_fingerprints(self) -> Any:
|
|
18
|
-
with self.path.open("r", encoding="utf-8") as f:
|
|
19
|
-
return json.load(f)
|
|
8
|
+
def __init__(self, version_data: ApkBuildFingerprint) -> None:
|
|
9
|
+
self.data = version_data
|
|
20
10
|
|
|
21
11
|
def generate_fingerprint(
|
|
22
12
|
self,
|
|
23
|
-
version: str,
|
|
24
13
|
device_id: str,
|
|
25
14
|
calls_seed: int,
|
|
26
15
|
arch: str = "arm64-v8a",
|
|
27
16
|
) -> bytes | None:
|
|
28
|
-
data = self.data.get(version)
|
|
29
|
-
if not data:
|
|
30
|
-
return None
|
|
31
|
-
|
|
32
|
-
model = ApkBuildFingerprint.model_validate(data)
|
|
33
|
-
|
|
34
17
|
seed_bytes = struct.pack(">q", calls_seed)
|
|
35
18
|
device_bytes = device_id.encode("utf-8")
|
|
36
19
|
|
|
37
20
|
h1 = hashlib.sha256(
|
|
38
|
-
bytes.fromhex(
|
|
21
|
+
bytes.fromhex(self.data.certificate_meta_sha256) + seed_bytes + device_bytes
|
|
39
22
|
).digest()
|
|
40
23
|
h2 = hashlib.sha256(
|
|
41
|
-
bytes.fromhex(
|
|
24
|
+
bytes.fromhex(self.data.dex_meta_sha256) + seed_bytes + device_bytes
|
|
42
25
|
).digest()
|
|
43
26
|
h3 = hashlib.sha256(
|
|
44
|
-
bytes.fromhex(
|
|
27
|
+
bytes.fromhex(self.data.so_meta_sha256[arch]) + seed_bytes + device_bytes
|
|
45
28
|
).digest()
|
|
46
29
|
|
|
47
30
|
return h1 + h2 + h3
|
pymax/fingerprint/models.py
CHANGED
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
|