maxapi-python 2.1.3__py3-none-any.whl → 2.2.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.1.3.dist-info → maxapi_python-2.2.0.dist-info}/METADATA +3 -11
- {maxapi_python-2.1.3.dist-info → maxapi_python-2.2.0.dist-info}/RECORD +58 -53
- pymax/__init__.py +18 -3
- pymax/api/auth/payloads.py +7 -0
- pymax/api/auth/service.py +33 -30
- pymax/api/binding.py +57 -0
- pymax/api/chats/service.py +34 -47
- pymax/api/messages/enums.py +1 -0
- pymax/api/messages/payloads.py +16 -1
- pymax/api/messages/service.py +85 -33
- pymax/api/models.py +4 -6
- pymax/api/response.py +2 -2
- pymax/api/self/service.py +17 -26
- pymax/api/session/payloads.py +2 -9
- pymax/api/session/service.py +1 -3
- pymax/api/uploads/payloads.py +3 -9
- pymax/api/uploads/service.py +33 -99
- pymax/api/users/service.py +8 -16
- pymax/app.py +2 -0
- pymax/auth/qr.py +3 -9
- pymax/auth/sms.py +23 -11
- pymax/base.py +38 -1
- pymax/client.py +2 -1
- pymax/client_web.py +1 -2
- pymax/config.py +42 -3
- pymax/connection/connection.py +2 -0
- pymax/connection/readers/tcp.py +1 -3
- pymax/dispatch/dispatcher.py +36 -18
- pymax/dispatch/enums.py +4 -0
- pymax/dispatch/mapping.py +34 -11
- pymax/dispatch/resolvers.py +18 -0
- pymax/dispatch/router.py +34 -0
- pymax/formatting/markdown.py +22 -13
- pymax/infra/chat.py +12 -0
- pymax/infra/message.py +74 -3
- pymax/logging.py +2 -0
- pymax/protocol/tcp/compression.py +1 -3
- pymax/protocol/tcp/framing.py +1 -3
- pymax/protocol/ws/protocol.py +3 -9
- pymax/session/protocol.py +2 -6
- pymax/session/store.py +8 -24
- pymax/telemetry/navigation.py +1 -3
- pymax/telemetry/service.py +5 -17
- pymax/transport/tcp.py +1 -3
- pymax/types/domain/attachments/unknown.py +1 -3
- pymax/types/domain/auth.py +24 -2
- pymax/types/domain/chat.py +38 -1
- pymax/types/domain/message.py +31 -1
- pymax/types/domain/presence.py +3 -3
- pymax/types/domain/sync.py +5 -21
- pymax/types/events/__init__.py +4 -0
- pymax/types/events/mark.py +23 -0
- pymax/types/events/message.py +57 -5
- pymax/types/events/presence.py +15 -0
- pymax/types/events/reaction.py +21 -0
- pymax/types/events/typing.py +14 -0
- {maxapi_python-2.1.3.dist-info → maxapi_python-2.2.0.dist-info}/WHEEL +0 -0
- {maxapi_python-2.1.3.dist-info → maxapi_python-2.2.0.dist-info}/licenses/LICENSE +0 -0
pymax/dispatch/dispatcher.py
CHANGED
|
@@ -9,6 +9,12 @@ from pymax.logging import get_logger
|
|
|
9
9
|
from pymax.protocol import InboundFrame
|
|
10
10
|
from pymax.types import Chat, MessageDeleteEvent
|
|
11
11
|
from pymax.types.domain import Message
|
|
12
|
+
from pymax.types.events import (
|
|
13
|
+
MessageReadEvent,
|
|
14
|
+
PresenceEvent,
|
|
15
|
+
ReactionUpdateEvent,
|
|
16
|
+
TypingEvent,
|
|
17
|
+
)
|
|
12
18
|
|
|
13
19
|
from .enums import EventType
|
|
14
20
|
from .mapping import EventMapper, EventResolver
|
|
@@ -33,9 +39,7 @@ ClientT = TypeVar("ClientT")
|
|
|
33
39
|
|
|
34
40
|
|
|
35
41
|
class Dispatcher(Generic[ClientT]):
|
|
36
|
-
def __init__(
|
|
37
|
-
self, app: App, root_router: Router[ClientT] | None = None
|
|
38
|
-
) -> None:
|
|
42
|
+
def __init__(self, app: App, root_router: Router[ClientT] | None = None) -> None:
|
|
39
43
|
self.root_router: Router[ClientT] = root_router or Router()
|
|
40
44
|
self.internal_router: Router[ClientT] = Router()
|
|
41
45
|
self.resolver = EventResolver()
|
|
@@ -71,9 +75,7 @@ class Dispatcher(Generic[ClientT]):
|
|
|
71
75
|
event: EventType,
|
|
72
76
|
*filters: FilterCallback[Any],
|
|
73
77
|
) -> HandlerDecorator[Any, ClientT]:
|
|
74
|
-
logger.debug(
|
|
75
|
-
"registering handler event=%s filters=%s", event, len(filters)
|
|
76
|
-
)
|
|
78
|
+
logger.debug("registering handler event=%s filters=%s", event, len(filters))
|
|
77
79
|
return self.root_router.on(event, *filters)
|
|
78
80
|
|
|
79
81
|
def on_message(
|
|
@@ -87,9 +89,7 @@ class Dispatcher(Generic[ClientT]):
|
|
|
87
89
|
self,
|
|
88
90
|
*filters: FilterCallback[Message],
|
|
89
91
|
) -> HandlerDecorator[Message, ClientT]:
|
|
90
|
-
logger.debug(
|
|
91
|
-
"registering message edit handler filters=%s", len(filters)
|
|
92
|
-
)
|
|
92
|
+
logger.debug("registering message edit handler filters=%s", len(filters))
|
|
93
93
|
return self.root_router.on_message_edit(*filters)
|
|
94
94
|
|
|
95
95
|
def on_message_delete(
|
|
@@ -98,6 +98,30 @@ class Dispatcher(Generic[ClientT]):
|
|
|
98
98
|
) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
|
|
99
99
|
return self.root_router.on_message_delete(*filters)
|
|
100
100
|
|
|
101
|
+
def on_message_read(
|
|
102
|
+
self,
|
|
103
|
+
*filters: FilterCallback[MessageReadEvent],
|
|
104
|
+
) -> HandlerDecorator[MessageReadEvent, ClientT]:
|
|
105
|
+
return self.root_router.on_message_read(*filters)
|
|
106
|
+
|
|
107
|
+
def on_typing(
|
|
108
|
+
self,
|
|
109
|
+
*filters: FilterCallback[TypingEvent],
|
|
110
|
+
) -> HandlerDecorator[TypingEvent, ClientT]:
|
|
111
|
+
return self.root_router.on_typing(*filters)
|
|
112
|
+
|
|
113
|
+
def on_presence(
|
|
114
|
+
self,
|
|
115
|
+
*filters: FilterCallback[PresenceEvent],
|
|
116
|
+
) -> HandlerDecorator[PresenceEvent, ClientT]:
|
|
117
|
+
return self.root_router.on_presence(*filters)
|
|
118
|
+
|
|
119
|
+
def on_reaction_update(
|
|
120
|
+
self,
|
|
121
|
+
*filters: FilterCallback[ReactionUpdateEvent],
|
|
122
|
+
) -> HandlerDecorator[ReactionUpdateEvent, ClientT]:
|
|
123
|
+
return self.root_router.on_reaction_update(*filters)
|
|
124
|
+
|
|
101
125
|
def on_chat_update(
|
|
102
126
|
self,
|
|
103
127
|
*filters: FilterCallback[Chat],
|
|
@@ -116,9 +140,7 @@ class Dispatcher(Generic[ClientT]):
|
|
|
116
140
|
def iter_routers(self) -> Generator[Router[ClientT], Any, None]:
|
|
117
141
|
yield from self._iter_router(self.root_router)
|
|
118
142
|
|
|
119
|
-
def _iter_router(
|
|
120
|
-
self, router: Router[ClientT]
|
|
121
|
-
) -> Generator[Router[ClientT], Any, None]:
|
|
143
|
+
def _iter_router(self, router: Router[ClientT]) -> Generator[Router[ClientT], Any, None]:
|
|
122
144
|
yield router
|
|
123
145
|
|
|
124
146
|
for child in router.children:
|
|
@@ -161,9 +183,7 @@ class Dispatcher(Generic[ClientT]):
|
|
|
161
183
|
if event_type is not None:
|
|
162
184
|
logger.debug("dispatching event type=%s", event_type)
|
|
163
185
|
event = self.mapper.map(event_type, frame)
|
|
164
|
-
await self._dispatch_to_router(
|
|
165
|
-
self.internal_router, event_type, event
|
|
166
|
-
)
|
|
186
|
+
await self._dispatch_to_router(self.internal_router, event_type, event)
|
|
167
187
|
await self._dispatch_to_router(self.root_router, event_type, event)
|
|
168
188
|
else:
|
|
169
189
|
logger.debug(
|
|
@@ -209,9 +229,7 @@ class Dispatcher(Generic[ClientT]):
|
|
|
209
229
|
return False
|
|
210
230
|
return True
|
|
211
231
|
|
|
212
|
-
async def _call(
|
|
213
|
-
self, callback: HandlerCallback[Any, ClientT], event: Any
|
|
214
|
-
) -> Any:
|
|
232
|
+
async def _call(self, callback: HandlerCallback[Any, ClientT], event: Any) -> Any:
|
|
215
233
|
if self.client is None:
|
|
216
234
|
raise RuntimeError("client is not bound")
|
|
217
235
|
|
pymax/dispatch/enums.py
CHANGED
|
@@ -5,6 +5,10 @@ class EventType(str, Enum):
|
|
|
5
5
|
MESSAGE_NEW = "message_new"
|
|
6
6
|
MESSAGE_EDIT = "message_edit"
|
|
7
7
|
MESSAGE_DELETE = "message_delete"
|
|
8
|
+
MESSAGE_READ = "message_read"
|
|
9
|
+
TYPING = "typing"
|
|
10
|
+
PRESENCE = "presence"
|
|
11
|
+
REACTION_UPDATE = "reaction_update"
|
|
8
12
|
CHAT_UPDATE = "chat_update"
|
|
9
13
|
USER_UPDATE = "user_update"
|
|
10
14
|
VIDEO_READY = "video_ready"
|
pymax/dispatch/mapping.py
CHANGED
|
@@ -3,11 +3,19 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
|
+
from pymax.api.binding import bind_api_model
|
|
6
7
|
from pymax.protocol import InboundFrame, Opcode
|
|
7
8
|
from pymax.protocol.enums import Command
|
|
8
9
|
from pymax.types import Chat, MessageDeleteEvent
|
|
9
10
|
from pymax.types.domain import Message
|
|
10
|
-
from pymax.types.events import
|
|
11
|
+
from pymax.types.events import (
|
|
12
|
+
FileUploadSignal,
|
|
13
|
+
MessageReadEvent,
|
|
14
|
+
PresenceEvent,
|
|
15
|
+
ReactionUpdateEvent,
|
|
16
|
+
TypingEvent,
|
|
17
|
+
VideoUploadSignal,
|
|
18
|
+
)
|
|
11
19
|
|
|
12
20
|
from .enums import EventType
|
|
13
21
|
from .resolvers import (
|
|
@@ -15,6 +23,10 @@ from .resolvers import (
|
|
|
15
23
|
resolve_chat,
|
|
16
24
|
resolve_message,
|
|
17
25
|
resolve_message_delete,
|
|
26
|
+
resolve_message_read,
|
|
27
|
+
resolve_presence,
|
|
28
|
+
resolve_reaction_update,
|
|
29
|
+
resolve_typing,
|
|
18
30
|
)
|
|
19
31
|
|
|
20
32
|
if TYPE_CHECKING:
|
|
@@ -28,6 +40,10 @@ EVENT_MAP: dict[Opcode, Resolver] = {
|
|
|
28
40
|
Opcode.NOTIF_CHAT: resolve_chat,
|
|
29
41
|
Opcode.NOTIF_MSG_DELETE: resolve_message_delete,
|
|
30
42
|
Opcode.NOTIF_ATTACH: resolve_attach,
|
|
43
|
+
Opcode.NOTIF_TYPING: resolve_typing,
|
|
44
|
+
Opcode.NOTIF_MARK: resolve_message_read,
|
|
45
|
+
Opcode.NOTIF_PRESENCE: resolve_presence,
|
|
46
|
+
Opcode.NOTIF_MSG_REACTIONS_CHANGED: resolve_reaction_update,
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
|
|
@@ -58,21 +74,28 @@ class EventMapper:
|
|
|
58
74
|
|
|
59
75
|
if frame.payload:
|
|
60
76
|
if event_type in (EventType.MESSAGE_NEW, EventType.MESSAGE_EDIT):
|
|
61
|
-
return
|
|
62
|
-
self.app
|
|
77
|
+
return bind_api_model(
|
|
78
|
+
self.app,
|
|
79
|
+
Message.model_validate(frame.payload),
|
|
63
80
|
)
|
|
64
81
|
elif event_type == EventType.CHAT_UPDATE:
|
|
65
|
-
return
|
|
66
|
-
self.app
|
|
67
|
-
|
|
82
|
+
return bind_api_model(
|
|
83
|
+
self.app,
|
|
84
|
+
Chat.model_validate(frame.payload["chat"]),
|
|
68
85
|
)
|
|
69
86
|
elif event_type == EventType.MESSAGE_DELETE:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
self.app.api.chats,
|
|
87
|
+
return bind_api_model(
|
|
88
|
+
self.app,
|
|
89
|
+
MessageDeleteEvent.model_validate(frame.payload),
|
|
74
90
|
)
|
|
75
|
-
|
|
91
|
+
elif event_type == EventType.MESSAGE_READ:
|
|
92
|
+
return MessageReadEvent.model_validate(frame.payload)
|
|
93
|
+
elif event_type == EventType.TYPING:
|
|
94
|
+
return TypingEvent.model_validate(frame.payload)
|
|
95
|
+
elif event_type == EventType.PRESENCE:
|
|
96
|
+
return PresenceEvent.model_validate(frame.payload)
|
|
97
|
+
elif event_type == EventType.REACTION_UPDATE:
|
|
98
|
+
return ReactionUpdateEvent.model_validate(frame.payload)
|
|
76
99
|
elif event_type == EventType.VIDEO_READY:
|
|
77
100
|
return VideoUploadSignal.model_validate(frame.payload)
|
|
78
101
|
elif event_type == EventType.FILE_READY:
|
pymax/dispatch/resolvers.py
CHANGED
|
@@ -20,6 +20,22 @@ def resolve_message_delete(_: InboundFrame) -> EventType | None:
|
|
|
20
20
|
return EventType.MESSAGE_DELETE
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def resolve_message_read(_: InboundFrame) -> EventType | None:
|
|
24
|
+
return EventType.MESSAGE_READ
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_typing(_: InboundFrame) -> EventType | None:
|
|
28
|
+
return EventType.TYPING
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_presence(_: InboundFrame) -> EventType | None:
|
|
32
|
+
return EventType.PRESENCE
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve_reaction_update(_: InboundFrame) -> EventType | None:
|
|
36
|
+
return EventType.REACTION_UPDATE
|
|
37
|
+
|
|
38
|
+
|
|
23
39
|
def resolve_attach(frame: InboundFrame) -> EventType | None:
|
|
24
40
|
try:
|
|
25
41
|
FileUploadSignal.model_validate(frame.payload)
|
|
@@ -45,6 +61,8 @@ def resolve_message(frame: InboundFrame) -> EventType | None:
|
|
|
45
61
|
|
|
46
62
|
if model.status == MessageStatus.EDITED:
|
|
47
63
|
return EventType.MESSAGE_EDIT
|
|
64
|
+
if model.status == MessageStatus.REMOVED:
|
|
65
|
+
return EventType.MESSAGE_DELETE
|
|
48
66
|
else:
|
|
49
67
|
return EventType.MESSAGE_NEW
|
|
50
68
|
except ValidationError:
|
pymax/dispatch/router.py
CHANGED
|
@@ -14,6 +14,12 @@ if TYPE_CHECKING:
|
|
|
14
14
|
from pymax.protocol import InboundFrame
|
|
15
15
|
from pymax.types import Chat
|
|
16
16
|
from pymax.types.domain import Message
|
|
17
|
+
from pymax.types.events import (
|
|
18
|
+
MessageReadEvent,
|
|
19
|
+
PresenceEvent,
|
|
20
|
+
ReactionUpdateEvent,
|
|
21
|
+
TypingEvent,
|
|
22
|
+
)
|
|
17
23
|
|
|
18
24
|
|
|
19
25
|
_EventT = TypeVar("_EventT")
|
|
@@ -186,6 +192,34 @@ class Router(Generic[ClientT]):
|
|
|
186
192
|
"""
|
|
187
193
|
return self.on(EventType.MESSAGE_DELETE, *filters)
|
|
188
194
|
|
|
195
|
+
def on_message_read(
|
|
196
|
+
self,
|
|
197
|
+
*filters: FilterCallback[MessageReadEvent],
|
|
198
|
+
) -> HandlerDecorator[MessageReadEvent, ClientT]:
|
|
199
|
+
"""Регистрирует обработчик изменения отметки прочтения."""
|
|
200
|
+
return self.on(EventType.MESSAGE_READ, *filters)
|
|
201
|
+
|
|
202
|
+
def on_typing(
|
|
203
|
+
self,
|
|
204
|
+
*filters: FilterCallback[TypingEvent],
|
|
205
|
+
) -> HandlerDecorator[TypingEvent, ClientT]:
|
|
206
|
+
"""Регистрирует обработчик набора текста."""
|
|
207
|
+
return self.on(EventType.TYPING, *filters)
|
|
208
|
+
|
|
209
|
+
def on_presence(
|
|
210
|
+
self,
|
|
211
|
+
*filters: FilterCallback[PresenceEvent],
|
|
212
|
+
) -> HandlerDecorator[PresenceEvent, ClientT]:
|
|
213
|
+
"""Регистрирует обработчик изменения присутствия пользователя."""
|
|
214
|
+
return self.on(EventType.PRESENCE, *filters)
|
|
215
|
+
|
|
216
|
+
def on_reaction_update(
|
|
217
|
+
self,
|
|
218
|
+
*filters: FilterCallback[ReactionUpdateEvent],
|
|
219
|
+
) -> HandlerDecorator[ReactionUpdateEvent, ClientT]:
|
|
220
|
+
"""Регистрирует обработчик обновления реакций сообщения."""
|
|
221
|
+
return self.on(EventType.REACTION_UPDATE, *filters)
|
|
222
|
+
|
|
189
223
|
def on_chat_update(
|
|
190
224
|
self,
|
|
191
225
|
*filters: FilterCallback[Chat],
|
pymax/formatting/markdown.py
CHANGED
|
@@ -2,6 +2,10 @@ from pymax.types.domain.element import Element, ElementAttributes
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class Formatter:
|
|
5
|
+
# Characters above this value are encoded as surrogate pairs in UTF-16,
|
|
6
|
+
# occupying 2 code units instead of 1.
|
|
7
|
+
BMP_MAX = 0xFFFF
|
|
8
|
+
|
|
5
9
|
MARKERS = {
|
|
6
10
|
"```": "CODE",
|
|
7
11
|
"**": "STRONG",
|
|
@@ -14,6 +18,10 @@ class Formatter:
|
|
|
14
18
|
|
|
15
19
|
MARKER_ORDER = ["```", "**", "__", "~~", "`", "_", "*"]
|
|
16
20
|
|
|
21
|
+
@staticmethod
|
|
22
|
+
def _code_units_len(text: str) -> int:
|
|
23
|
+
return len(text.encode("utf-16-le")) // 2
|
|
24
|
+
|
|
17
25
|
@staticmethod
|
|
18
26
|
def _parse_link(
|
|
19
27
|
text: str,
|
|
@@ -64,15 +72,16 @@ class Formatter:
|
|
|
64
72
|
label, url, next_i = parsed_link
|
|
65
73
|
|
|
66
74
|
start = clean_pos
|
|
75
|
+
utf16_label_len = Formatter._code_units_len(label)
|
|
67
76
|
|
|
68
77
|
clean_text += label
|
|
69
|
-
clean_pos +=
|
|
78
|
+
clean_pos += utf16_label_len
|
|
70
79
|
|
|
71
80
|
entities.append(
|
|
72
81
|
Element(
|
|
73
82
|
type="LINK",
|
|
74
83
|
from_=start,
|
|
75
|
-
length=
|
|
84
|
+
length=utf16_label_len,
|
|
76
85
|
attributes=ElementAttributes(url=url),
|
|
77
86
|
)
|
|
78
87
|
)
|
|
@@ -93,9 +102,10 @@ class Formatter:
|
|
|
93
102
|
start = clean_pos
|
|
94
103
|
|
|
95
104
|
while i < len(text) and text[i] != "\n":
|
|
96
|
-
|
|
105
|
+
ch = text[i]
|
|
106
|
+
clean_text += ch
|
|
97
107
|
i += 1
|
|
98
|
-
clean_pos += 1
|
|
108
|
+
clean_pos += 2 if ord(ch) > Formatter.BMP_MAX else 1
|
|
99
109
|
|
|
100
110
|
length = clean_pos - start
|
|
101
111
|
|
|
@@ -123,9 +133,10 @@ class Formatter:
|
|
|
123
133
|
start = clean_pos
|
|
124
134
|
|
|
125
135
|
while i < len(text) and text[i] != "\n":
|
|
126
|
-
|
|
136
|
+
ch = text[i]
|
|
137
|
+
clean_text += ch
|
|
127
138
|
i += 1
|
|
128
|
-
clean_pos += 1
|
|
139
|
+
clean_pos += 2 if ord(ch) > Formatter.BMP_MAX else 1
|
|
129
140
|
|
|
130
141
|
length = clean_pos - start
|
|
131
142
|
|
|
@@ -151,10 +162,7 @@ class Formatter:
|
|
|
151
162
|
if marker == "```":
|
|
152
163
|
closing_index = text.find(marker, i + marker_len)
|
|
153
164
|
|
|
154
|
-
if
|
|
155
|
-
closing_index == -1
|
|
156
|
-
or closing_index == i + marker_len
|
|
157
|
-
):
|
|
165
|
+
if closing_index == -1 or closing_index == i + marker_len:
|
|
158
166
|
clean_text += marker
|
|
159
167
|
clean_pos += marker_len
|
|
160
168
|
i += marker_len
|
|
@@ -211,10 +219,11 @@ class Formatter:
|
|
|
211
219
|
line_start = False
|
|
212
220
|
continue
|
|
213
221
|
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
ch = text[i]
|
|
223
|
+
clean_text += ch
|
|
224
|
+
line_start = ch == "\n"
|
|
216
225
|
|
|
217
226
|
i += 1
|
|
218
|
-
clean_pos += 1
|
|
227
|
+
clean_pos += 2 if ord(ch) > Formatter.BMP_MAX else 1
|
|
219
228
|
|
|
220
229
|
return clean_text, entities
|
pymax/infra/chat.py
CHANGED
|
@@ -339,3 +339,15 @@ class ChatMixin(IClientProtocol):
|
|
|
339
339
|
chat_id=chat_id,
|
|
340
340
|
user_id=user_id,
|
|
341
341
|
)
|
|
342
|
+
|
|
343
|
+
async def join_channel(self, link: str) -> Chat:
|
|
344
|
+
"""Вступает в канал по ссылке.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
link: Полная ссылка на канал, invite-ссылка или ее часть с
|
|
348
|
+
join-токеном Max.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Канал, в который вступил клиент.
|
|
352
|
+
"""
|
|
353
|
+
return await self._app.api.chats.join_channel(link=link)
|
pymax/infra/message.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from pymax.api.messages.enums import ItemType
|
|
2
|
-
from pymax.api.messages.service import SendAttachments
|
|
2
|
+
from pymax.api.messages.service import SendAttachment, SendAttachments
|
|
3
3
|
from pymax.types import (
|
|
4
4
|
FileRequest,
|
|
5
5
|
Message,
|
|
@@ -43,6 +43,73 @@ class MessageMixin(IClientProtocol):
|
|
|
43
43
|
notify=notify,
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
+
async def get_message(
|
|
47
|
+
self,
|
|
48
|
+
chat_id: int,
|
|
49
|
+
message_id: int,
|
|
50
|
+
) -> Message | None:
|
|
51
|
+
"""Возвращает сообщение по ID.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
chat_id: ID чата.
|
|
55
|
+
message_id: ID сообщения.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Сообщение или ``None``, если сервер его не вернул.
|
|
59
|
+
"""
|
|
60
|
+
return await self._app.api.messages.get_message(
|
|
61
|
+
chat_id=chat_id,
|
|
62
|
+
message_id=message_id,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
async def get_messages(
|
|
66
|
+
self,
|
|
67
|
+
chat_id: int,
|
|
68
|
+
message_ids: list[int],
|
|
69
|
+
) -> list[Message]:
|
|
70
|
+
"""Возвращает сообщения по ID.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
chat_id: ID чата.
|
|
74
|
+
message_ids: ID сообщений.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Список найденных сообщений.
|
|
78
|
+
"""
|
|
79
|
+
return await self._app.api.messages.get_messages(
|
|
80
|
+
chat_id=chat_id,
|
|
81
|
+
message_ids=message_ids,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def edit_message(
|
|
85
|
+
self,
|
|
86
|
+
chat_id: int,
|
|
87
|
+
message_id: int,
|
|
88
|
+
text: str,
|
|
89
|
+
attachment: SendAttachment | None = None,
|
|
90
|
+
attachments: SendAttachments = None,
|
|
91
|
+
) -> Message:
|
|
92
|
+
"""Редактирует текст и вложения сообщения.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
chat_id: ID чата.
|
|
96
|
+
message_id: ID сообщения.
|
|
97
|
+
text: Новый текст сообщения с поддержкой markdown.
|
|
98
|
+
attachment: Одно новое вложение.
|
|
99
|
+
attachments: Список новых вложений. Имеет приоритет над
|
|
100
|
+
``attachment``.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Отредактированное сообщение.
|
|
104
|
+
"""
|
|
105
|
+
return await self._app.api.messages.edit_message(
|
|
106
|
+
chat_id=chat_id,
|
|
107
|
+
message_id=message_id,
|
|
108
|
+
text=text,
|
|
109
|
+
attachment=attachment,
|
|
110
|
+
attachments=attachments,
|
|
111
|
+
)
|
|
112
|
+
|
|
46
113
|
async def fetch_history(
|
|
47
114
|
self,
|
|
48
115
|
chat_id: int,
|
|
@@ -236,11 +303,15 @@ class MessageMixin(IClientProtocol):
|
|
|
236
303
|
message_id=message_id,
|
|
237
304
|
)
|
|
238
305
|
|
|
239
|
-
async def read_message(self, message_id: int, chat_id: int) -> ReadState:
|
|
306
|
+
async def read_message(self, message_id: int | str, chat_id: int) -> ReadState:
|
|
240
307
|
"""Отмечает сообщение как прочитанное.
|
|
241
308
|
|
|
309
|
+
У Max различается wire-формат ``message_id`` для отметки прочтения:
|
|
310
|
+
TCP-клиент ожидает ``int``, WebSocket-клиент - ``str``.
|
|
311
|
+
|
|
242
312
|
Args:
|
|
243
|
-
message_id: ID сообщения.
|
|
313
|
+
message_id: ID сообщения. Передавайте ``int`` для ``Client`` и
|
|
314
|
+
``str`` для ``WebClient``.
|
|
244
315
|
chat_id: ID чата.
|
|
245
316
|
|
|
246
317
|
Returns:
|
pymax/logging.py
CHANGED
|
@@ -85,6 +85,8 @@ def configure_logging(
|
|
|
85
85
|
configure_logging("DEBUG", use_colors=False)
|
|
86
86
|
"""
|
|
87
87
|
stream = stream or sys.stderr
|
|
88
|
+
if stream is None:
|
|
89
|
+
raise RuntimeError("No logging stream is available")
|
|
88
90
|
|
|
89
91
|
if use_colors is None:
|
|
90
92
|
use_colors = hasattr(stream, "isatty") and stream.isatty()
|
pymax/protocol/tcp/framing.py
CHANGED
|
@@ -31,9 +31,7 @@ class TcpPacketFramer:
|
|
|
31
31
|
if len(data) < self.HEADER_SIZE:
|
|
32
32
|
return None
|
|
33
33
|
|
|
34
|
-
ver, cmd, seq, opcode, packed_len = self.HEADER_STRUCT.unpack_from(
|
|
35
|
-
data, 0
|
|
36
|
-
)
|
|
34
|
+
ver, cmd, seq, opcode, packed_len = self.HEADER_STRUCT.unpack_from(data, 0)
|
|
37
35
|
flags = (packed_len >> 24) & 0xFF
|
|
38
36
|
payload_len = packed_len & 0x00FFFFFF
|
|
39
37
|
|
pymax/protocol/ws/protocol.py
CHANGED
|
@@ -20,14 +20,8 @@ class WsProtocol(BaseProtocol):
|
|
|
20
20
|
data = json.loads(raw)
|
|
21
21
|
return InboundFrame.model_validate(data)
|
|
22
22
|
except json.JSONDecodeError:
|
|
23
|
-
logger.debug(
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
return InboundFrame(
|
|
27
|
-
opcode=0, cmd=0, seq=None, payload=None, raw=None
|
|
28
|
-
)
|
|
23
|
+
logger.debug("failed to decode websocket frame json", exc_info=True)
|
|
24
|
+
return InboundFrame(opcode=0, cmd=0, seq=None, payload=None, raw=None)
|
|
29
25
|
except ValidationError:
|
|
30
26
|
logger.debug("failed to validate websocket frame", exc_info=True)
|
|
31
|
-
return InboundFrame(
|
|
32
|
-
opcode=0, cmd=0, seq=None, payload=None, raw=None
|
|
33
|
-
)
|
|
27
|
+
return InboundFrame(opcode=0, cmd=0, seq=None, payload=None, raw=None)
|
pymax/session/protocol.py
CHANGED
|
@@ -8,11 +8,7 @@ class StoreProtocol(Protocol):
|
|
|
8
8
|
async def save_session(self, session_info: SessionInfo) -> None: ...
|
|
9
9
|
async def update_token(self, old_token: str, new_token: str) -> None: ...
|
|
10
10
|
async def load_session(self) -> SessionInfo | None: ...
|
|
11
|
-
async def load_session_by_device_id(
|
|
12
|
-
|
|
13
|
-
) -> SessionInfo | None: ...
|
|
14
|
-
async def load_session_by_phone(
|
|
15
|
-
self, phone: str
|
|
16
|
-
) -> SessionInfo | None: ...
|
|
11
|
+
async def load_session_by_device_id(self, device_id: str) -> SessionInfo | None: ...
|
|
12
|
+
async def load_session_by_phone(self, phone: str) -> SessionInfo | None: ...
|
|
17
13
|
async def delete_session(self, token: str) -> None: ...
|
|
18
14
|
async def close(self) -> None: ...
|
pymax/session/store.py
CHANGED
|
@@ -55,24 +55,12 @@ class SessionStore:
|
|
|
55
55
|
)
|
|
56
56
|
"""
|
|
57
57
|
)
|
|
58
|
-
await self._ensure_column(
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
await self._ensure_column(
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
await self._ensure_column(
|
|
65
|
-
conn, "contacts_sync", "INTEGER NOT NULL DEFAULT -1"
|
|
66
|
-
)
|
|
67
|
-
await self._ensure_column(
|
|
68
|
-
conn, "drafts_sync", "INTEGER NOT NULL DEFAULT -1"
|
|
69
|
-
)
|
|
70
|
-
await self._ensure_column(
|
|
71
|
-
conn, "presence_sync", "INTEGER NOT NULL DEFAULT -1"
|
|
72
|
-
)
|
|
73
|
-
await self._ensure_column(
|
|
74
|
-
conn, "config_hash", "TEXT NOT NULL DEFAULT ''"
|
|
75
|
-
)
|
|
58
|
+
await self._ensure_column(conn, "mt_instance_id", "TEXT NOT NULL DEFAULT ''")
|
|
59
|
+
await self._ensure_column(conn, "chats_sync", "INTEGER NOT NULL DEFAULT -1")
|
|
60
|
+
await self._ensure_column(conn, "contacts_sync", "INTEGER NOT NULL DEFAULT -1")
|
|
61
|
+
await self._ensure_column(conn, "drafts_sync", "INTEGER NOT NULL DEFAULT -1")
|
|
62
|
+
await self._ensure_column(conn, "presence_sync", "INTEGER NOT NULL DEFAULT -1")
|
|
63
|
+
await self._ensure_column(conn, "config_hash", "TEXT NOT NULL DEFAULT ''")
|
|
76
64
|
await conn.execute(
|
|
77
65
|
"""
|
|
78
66
|
UPDATE sessions
|
|
@@ -93,9 +81,7 @@ class SessionStore:
|
|
|
93
81
|
columns = {row["name"] for row in await cursor.fetchall()}
|
|
94
82
|
|
|
95
83
|
if name not in columns:
|
|
96
|
-
await conn.execute(
|
|
97
|
-
f"ALTER TABLE sessions ADD COLUMN {name} {definition}"
|
|
98
|
-
)
|
|
84
|
+
await conn.execute(f"ALTER TABLE sessions ADD COLUMN {name} {definition}")
|
|
99
85
|
|
|
100
86
|
async def save_session(self, session_info: SessionInfo) -> None:
|
|
101
87
|
conn = await self._get_connection()
|
|
@@ -158,9 +144,7 @@ class SessionStore:
|
|
|
158
144
|
)
|
|
159
145
|
return self._row_to_session(row)
|
|
160
146
|
|
|
161
|
-
async def load_session_by_device_id(
|
|
162
|
-
self, device_id: str
|
|
163
|
-
) -> SessionInfo | None:
|
|
147
|
+
async def load_session_by_device_id(self, device_id: str) -> SessionInfo | None:
|
|
164
148
|
conn = await self._get_connection()
|
|
165
149
|
logger.debug("loading session by device_id=%s", device_id)
|
|
166
150
|
async with conn.execute(
|
pymax/telemetry/navigation.py
CHANGED
|
@@ -155,9 +155,7 @@ class NavigationPlanner:
|
|
|
155
155
|
self.current_screen = self.history.pop()
|
|
156
156
|
return self.current_screen
|
|
157
157
|
|
|
158
|
-
next_screen = self._weighted_choice(
|
|
159
|
-
self.rules.graph[self.current_screen]
|
|
160
|
-
)
|
|
158
|
+
next_screen = self._weighted_choice(self.rules.graph[self.current_screen])
|
|
161
159
|
if next_screen != self.current_screen:
|
|
162
160
|
self.history.append(self.current_screen)
|
|
163
161
|
if len(self.history) > 4:
|
pymax/telemetry/service.py
CHANGED
|
@@ -82,20 +82,14 @@ class TelemetryService:
|
|
|
82
82
|
async def _run(self) -> None:
|
|
83
83
|
try:
|
|
84
84
|
await asyncio.sleep(self._between(self._timing.startup_delay))
|
|
85
|
-
await self._send_events(
|
|
86
|
-
[self._payloads.login(self._user_id, self._session_id)]
|
|
87
|
-
)
|
|
85
|
+
await self._send_events([self._payloads.login(self._user_id, self._session_id)])
|
|
88
86
|
|
|
89
87
|
while True:
|
|
90
88
|
self._session_id += 1
|
|
91
|
-
events = await self._collect_session_events(
|
|
92
|
-
self._planner.new_profile()
|
|
93
|
-
)
|
|
89
|
+
events = await self._collect_session_events(self._planner.new_profile())
|
|
94
90
|
await self._send_events(events)
|
|
95
91
|
self._planner.reset_to_background()
|
|
96
|
-
await asyncio.sleep(
|
|
97
|
-
self._between(self._timing.session_idle_delay)
|
|
98
|
-
)
|
|
92
|
+
await asyncio.sleep(self._between(self._timing.session_idle_delay))
|
|
99
93
|
|
|
100
94
|
except asyncio.CancelledError:
|
|
101
95
|
raise
|
|
@@ -163,9 +157,7 @@ class TelemetryService:
|
|
|
163
157
|
except Exception:
|
|
164
158
|
logger.debug("telemetry send failed", exc_info=True)
|
|
165
159
|
|
|
166
|
-
def _nav_event(
|
|
167
|
-
self, screen_from: Screen, screen_to: Screen
|
|
168
|
-
) -> TelemetryEvent:
|
|
160
|
+
def _nav_event(self, screen_from: Screen, screen_to: Screen) -> TelemetryEvent:
|
|
169
161
|
event = self._payloads.navigation(
|
|
170
162
|
user_id=self._user_id,
|
|
171
163
|
session_id=self._session_id,
|
|
@@ -212,11 +204,7 @@ class TelemetryService:
|
|
|
212
204
|
|
|
213
205
|
@property
|
|
214
206
|
def _ready(self) -> bool:
|
|
215
|
-
return
|
|
216
|
-
self.app.started
|
|
217
|
-
and self.app.me is not None
|
|
218
|
-
and self.app.connection.is_open
|
|
219
|
-
)
|
|
207
|
+
return self.app.started and self.app.me is not None and self.app.connection.is_open
|
|
220
208
|
|
|
221
209
|
@property
|
|
222
210
|
def _user_id(self) -> int:
|
pymax/transport/tcp.py
CHANGED
|
@@ -10,9 +10,7 @@ logger = get_logger(__name__)
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TCPTransport(Transport):
|
|
13
|
-
def __init__(
|
|
14
|
-
self, host: str, port: int, proxy: str | None, use_ssl: bool = True
|
|
15
|
-
) -> None:
|
|
13
|
+
def __init__(self, host: str, port: int, proxy: str | None, use_ssl: bool = True) -> None:
|
|
16
14
|
self._host = host
|
|
17
15
|
self._port = port
|
|
18
16
|
self._proxy = proxy
|