maxapi-python 2.3.1__py3-none-any.whl → 2.4.1__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.3.1.dist-info → maxapi_python-2.4.1.dist-info}/METADATA +8 -4
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/RECORD +79 -66
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/WHEEL +1 -1
- pymax/__init__.py +9 -2
- pymax/_data/apk_fingerprints.json +767 -0
- pymax/_data/rootca_ssl_rsa2022.crt +33 -0
- pymax/api/auth/enums.py +1 -0
- pymax/api/auth/payloads.py +28 -2
- pymax/api/auth/service.py +100 -3
- pymax/api/chats/__init__.py +1 -0
- pymax/api/chats/enums.py +15 -0
- pymax/api/chats/payloads.py +18 -2
- pymax/api/chats/service.py +43 -1
- pymax/api/messages/__init__.py +1 -1
- pymax/api/messages/payloads.py +21 -7
- pymax/api/messages/service.py +161 -24
- pymax/api/self/__init__.py +2 -1
- pymax/api/self/enums.py +9 -0
- pymax/api/self/payloads.py +40 -1
- pymax/api/self/service.py +37 -0
- pymax/api/session/service.py +21 -8
- pymax/api/uploads/payloads.py +39 -1
- pymax/api/uploads/service.py +247 -55
- pymax/app.py +91 -18
- pymax/auth/__init__.py +2 -0
- pymax/auth/exceptions.py +9 -0
- pymax/auth/providers.py +6 -2
- pymax/auth/sms.py +11 -1
- pymax/base.py +90 -8
- pymax/client.py +39 -10
- pymax/client_web.py +17 -14
- pymax/config.py +98 -19
- pymax/connection/connection.py +29 -18
- pymax/dispatch/enums.py +1 -0
- pymax/dispatch/mapping.py +3 -1
- pymax/dispatch/resolvers.py +7 -1
- pymax/files/__init__.py +4 -1
- pymax/files/base.py +58 -1
- pymax/files/video.py +51 -1
- pymax/files/voice.py +53 -0
- pymax/fingerprint/__init__.py +1 -0
- pymax/fingerprint/fingerprint.py +30 -0
- pymax/fingerprint/models.py +12 -0
- pymax/infra/auth.py +4 -0
- pymax/infra/bots.py +3 -3
- pymax/infra/chat.py +47 -5
- pymax/infra/message.py +49 -14
- pymax/infra/protocol.py +5 -1
- pymax/infra/self.py +25 -1
- pymax/protocol/enums.py +13 -0
- pymax/protocol/tcp/payload.py +21 -1
- 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 +17 -4
- pymax/transport/websocket.py +6 -3
- pymax/types/domain/__init__.py +2 -1
- pymax/types/domain/attachments/__init__.py +2 -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 +33 -1
- pymax/types/domain/attachments/poll.py +111 -0
- pymax/types/domain/attachments/sticker.py +1 -1
- pymax/types/domain/attachments/video.py +32 -11
- pymax/types/domain/chat.py +9 -9
- pymax/types/domain/enums.py +7 -0
- pymax/types/domain/handshake.py +16 -0
- pymax/types/domain/login.py +30 -1
- pymax/types/domain/message.py +108 -27
- pymax/types/domain/user.py +1 -3
- pymax/types/events/__init__.py +1 -0
- pymax/types/events/reaction.py +2 -2
- pymax/types/events/voice.py +5 -0
- pymax/versions/__init__.py +2 -0
- pymax/versions/catalog.py +104 -0
- pymax/versions/exceptions.py +10 -0
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/licenses/LICENSE +0 -0
pymax/api/messages/service.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import time
|
|
4
5
|
from collections.abc import Sequence
|
|
5
|
-
from
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from typing import TYPE_CHECKING, TypeAlias, TypeVar
|
|
6
8
|
|
|
7
9
|
from pymax.api.binding import bind_api_model, bind_api_models
|
|
8
10
|
from pymax.api.response import (
|
|
@@ -16,15 +18,19 @@ from pymax.api.uploads.payloads import (
|
|
|
16
18
|
AttachFilePayload,
|
|
17
19
|
AttachPhotoPayload,
|
|
18
20
|
VideoAttachPayload,
|
|
21
|
+
VideoNoteAttachPayload,
|
|
22
|
+
VoiceAttachPayload,
|
|
19
23
|
)
|
|
20
|
-
from pymax.exceptions import UploadError
|
|
21
|
-
from pymax.files import File, Photo, Video
|
|
24
|
+
from pymax.exceptions import ApiError, UploadError
|
|
25
|
+
from pymax.files import File, Photo, Video, VideoNote, Voice
|
|
22
26
|
from pymax.formatting.markdown import Formatter
|
|
23
27
|
from pymax.logging import get_logger
|
|
24
28
|
from pymax.protocol import Opcode
|
|
25
29
|
from pymax.types.domain import (
|
|
26
30
|
FileRequest,
|
|
27
31
|
Message,
|
|
32
|
+
Poll,
|
|
33
|
+
PollState,
|
|
28
34
|
ReactionInfo,
|
|
29
35
|
ReadState,
|
|
30
36
|
VideoRequest,
|
|
@@ -34,6 +40,7 @@ from .enums import ItemType, MessagePayloadKey, ReadAction
|
|
|
34
40
|
from .payloads import (
|
|
35
41
|
AddReactionPayload,
|
|
36
42
|
ChatHistoryPayload,
|
|
43
|
+
DelayedAttributes,
|
|
37
44
|
DeleteMessagePayload,
|
|
38
45
|
EditMessagePayload,
|
|
39
46
|
ForwardLink,
|
|
@@ -50,13 +57,17 @@ from .payloads import (
|
|
|
50
57
|
ReplyLink,
|
|
51
58
|
SendMessagePayload,
|
|
52
59
|
SendMessagePayloadMessage,
|
|
60
|
+
VotePollPayload,
|
|
53
61
|
)
|
|
54
62
|
|
|
55
63
|
if TYPE_CHECKING:
|
|
56
64
|
from pymax.app import App
|
|
57
65
|
|
|
58
|
-
SendAttachment: TypeAlias = Photo | File | Video
|
|
66
|
+
SendAttachment: TypeAlias = Photo | File | Video | Poll | Voice | VideoNote
|
|
59
67
|
SendAttachments: TypeAlias = Sequence[SendAttachment] | None
|
|
68
|
+
DateTimeUnion: TypeAlias = datetime | timedelta | int
|
|
69
|
+
|
|
70
|
+
T = TypeVar("T")
|
|
60
71
|
|
|
61
72
|
logger = get_logger(__name__)
|
|
62
73
|
|
|
@@ -76,13 +87,25 @@ class MessageService:
|
|
|
76
87
|
|
|
77
88
|
async def _upload_attachments(
|
|
78
89
|
self, attachments: SendAttachments
|
|
79
|
-
) -> list[
|
|
80
|
-
|
|
90
|
+
) -> list[
|
|
91
|
+
AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | VoiceAttachPayload | Poll
|
|
92
|
+
]: # TODO: TypeAlias
|
|
93
|
+
result: list[
|
|
94
|
+
AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | VoiceAttachPayload | Poll
|
|
95
|
+
] = []
|
|
81
96
|
if not attachments:
|
|
82
97
|
return result
|
|
83
98
|
|
|
84
99
|
for attachment in attachments:
|
|
85
|
-
if isinstance(attachment,
|
|
100
|
+
if isinstance(attachment, Voice):
|
|
101
|
+
upload_result = await self.app.api.uploads.upload_voice(attachment)
|
|
102
|
+
if not upload_result:
|
|
103
|
+
logger.error("Voice uploading failed")
|
|
104
|
+
raise UploadError("Voice uploading failed")
|
|
105
|
+
|
|
106
|
+
result.append(upload_result)
|
|
107
|
+
|
|
108
|
+
elif isinstance(attachment, Photo):
|
|
86
109
|
upload_result = await self.app.api.uploads.upload_photo(attachment)
|
|
87
110
|
if not upload_result:
|
|
88
111
|
logger.error("Photo uploading failed")
|
|
@@ -90,7 +113,7 @@ class MessageService:
|
|
|
90
113
|
|
|
91
114
|
result.append(upload_result)
|
|
92
115
|
|
|
93
|
-
elif isinstance(attachment, Video):
|
|
116
|
+
elif isinstance(attachment, (Video, VideoNote)):
|
|
94
117
|
upload_result = await self.app.api.uploads.upload_video(attachment)
|
|
95
118
|
if not upload_result:
|
|
96
119
|
logger.error("Video uploading failed")
|
|
@@ -106,20 +129,89 @@ class MessageService:
|
|
|
106
129
|
|
|
107
130
|
result.append(upload_result)
|
|
108
131
|
|
|
132
|
+
elif isinstance(attachment, Poll):
|
|
133
|
+
result.append(attachment)
|
|
134
|
+
|
|
109
135
|
return result
|
|
110
136
|
|
|
137
|
+
def _convert_time(self, time: DateTimeUnion) -> int:
|
|
138
|
+
if isinstance(time, datetime):
|
|
139
|
+
return int(time.timestamp() * 1000)
|
|
140
|
+
|
|
141
|
+
if isinstance(time, timedelta):
|
|
142
|
+
return int((datetime.now() + time).timestamp() * 1000)
|
|
143
|
+
|
|
144
|
+
return time * 1000
|
|
145
|
+
|
|
146
|
+
async def _wait_for_upload_signal(
|
|
147
|
+
self,
|
|
148
|
+
waiters: dict[int, asyncio.Future[T]],
|
|
149
|
+
video_id: int,
|
|
150
|
+
) -> None:
|
|
151
|
+
loop = asyncio.get_running_loop()
|
|
152
|
+
future: asyncio.Future[T] = loop.create_future()
|
|
153
|
+
|
|
154
|
+
waiters[video_id] = future
|
|
155
|
+
try:
|
|
156
|
+
await asyncio.wait_for(future, timeout=60)
|
|
157
|
+
except TimeoutError:
|
|
158
|
+
logger.warning(
|
|
159
|
+
"Timed out waiting for video processing notification video_id=%s",
|
|
160
|
+
video_id,
|
|
161
|
+
)
|
|
162
|
+
raise UploadError(f"Timed out waiting for video processing video_id={video_id}")
|
|
163
|
+
finally:
|
|
164
|
+
waiters.pop(video_id, None)
|
|
165
|
+
|
|
166
|
+
async def _process_attachment_error(
|
|
167
|
+
self,
|
|
168
|
+
attaches: list[
|
|
169
|
+
AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | VoiceAttachPayload | Poll
|
|
170
|
+
],
|
|
171
|
+
):
|
|
172
|
+
attachment: VoiceAttachPayload | VideoNoteAttachPayload | None = None
|
|
173
|
+
for attach in attaches:
|
|
174
|
+
if isinstance(attach, (VoiceAttachPayload, VideoNoteAttachPayload)):
|
|
175
|
+
attachment = attach
|
|
176
|
+
break
|
|
177
|
+
|
|
178
|
+
if attachment is None:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
"Unexpected internal state: Non voice/video_note attach is not ready. "
|
|
181
|
+
+ "Please, report that issue"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if isinstance(attachment, VoiceAttachPayload):
|
|
185
|
+
await self._wait_for_upload_signal(
|
|
186
|
+
self.app.api.uploads.voice_upload_waiters, attachment.video_id
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
await self._wait_for_upload_signal(
|
|
190
|
+
self.app.api.uploads.video_upload_waiters, attachment.video_id
|
|
191
|
+
)
|
|
192
|
+
|
|
111
193
|
async def send_message(
|
|
112
194
|
self,
|
|
113
195
|
chat_id: int,
|
|
114
|
-
text: str,
|
|
196
|
+
text: str | None = None,
|
|
115
197
|
reply_to: int | None = None,
|
|
116
198
|
attachments: SendAttachments = None,
|
|
117
199
|
*,
|
|
118
200
|
notify: bool = True,
|
|
119
|
-
|
|
120
|
-
|
|
201
|
+
send_at: DateTimeUnion | None = None,
|
|
202
|
+
) -> Message:
|
|
203
|
+
logger.info("sending message chat_id=%s text_len=%s", chat_id, len(text) if text else 0)
|
|
204
|
+
|
|
205
|
+
if not text and not attachments:
|
|
206
|
+
logger.error("send_message failed: no text or attachments provided")
|
|
207
|
+
raise ValueError("Either text or attachments must be provided")
|
|
208
|
+
|
|
209
|
+
if text:
|
|
210
|
+
clean_text, elements = Formatter.format_markdown(text)
|
|
211
|
+
else:
|
|
212
|
+
clean_text, elements = None, []
|
|
121
213
|
|
|
122
|
-
|
|
214
|
+
attaches = await self._upload_attachments(attachments)
|
|
123
215
|
|
|
124
216
|
frame = SendMessagePayload(
|
|
125
217
|
chat_id=chat_id,
|
|
@@ -127,13 +219,26 @@ class MessageService:
|
|
|
127
219
|
text=clean_text,
|
|
128
220
|
cid=self._next_cid(),
|
|
129
221
|
elements=elements,
|
|
130
|
-
attaches=
|
|
222
|
+
attaches=attaches,
|
|
131
223
|
link=ReplyLink(message_id=reply_to) if reply_to else None,
|
|
224
|
+
delayed_attributes=DelayedAttributes(
|
|
225
|
+
time_to_fire=self._convert_time(send_at),
|
|
226
|
+
notify_sender=notify,
|
|
227
|
+
)
|
|
228
|
+
if send_at
|
|
229
|
+
else None,
|
|
132
230
|
),
|
|
133
231
|
notify=notify,
|
|
134
232
|
)
|
|
135
233
|
|
|
136
|
-
|
|
234
|
+
try:
|
|
235
|
+
response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
|
|
236
|
+
except ApiError as e:
|
|
237
|
+
if e.error == "attachment.not.ready":
|
|
238
|
+
await self._process_attachment_error(attaches)
|
|
239
|
+
response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
|
|
240
|
+
else:
|
|
241
|
+
raise
|
|
137
242
|
|
|
138
243
|
message = bind_api_model(
|
|
139
244
|
self.app,
|
|
@@ -149,7 +254,7 @@ class MessageService:
|
|
|
149
254
|
source_chat_id: int | None = None,
|
|
150
255
|
*,
|
|
151
256
|
notify: bool = True,
|
|
152
|
-
) -> Message
|
|
257
|
+
) -> Message:
|
|
153
258
|
source_chat_id = chat_id if source_chat_id is None else source_chat_id
|
|
154
259
|
logger.info(
|
|
155
260
|
"forwarding message source_chat_id=%s chat_id=%s message_id=%s",
|
|
@@ -208,19 +313,37 @@ class MessageService:
|
|
|
208
313
|
self,
|
|
209
314
|
chat_id: int,
|
|
210
315
|
message_id: int,
|
|
211
|
-
text: str,
|
|
316
|
+
text: str | None = None,
|
|
212
317
|
attachments: SendAttachments = None,
|
|
213
318
|
) -> Message:
|
|
214
|
-
|
|
319
|
+
if not text and not attachments:
|
|
320
|
+
logger.error("edit_message failed: no text or attachments provided")
|
|
321
|
+
raise ValueError("Either text or attachments must be provided")
|
|
322
|
+
|
|
323
|
+
if text:
|
|
324
|
+
clean_text, elements = Formatter.format_markdown(text)
|
|
325
|
+
else:
|
|
326
|
+
clean_text, elements = None, []
|
|
327
|
+
|
|
328
|
+
attaches = await self._upload_attachments(attachments)
|
|
329
|
+
|
|
215
330
|
frame = EditMessagePayload(
|
|
216
331
|
chat_id=chat_id,
|
|
217
332
|
message_id=message_id,
|
|
218
333
|
text=clean_text,
|
|
219
334
|
elements=elements,
|
|
220
|
-
attachments=
|
|
335
|
+
attachments=attaches,
|
|
221
336
|
)
|
|
337
|
+
try:
|
|
338
|
+
response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
|
|
339
|
+
except ApiError as e:
|
|
340
|
+
if e.error == "attachment.not.ready":
|
|
341
|
+
await self._process_attachment_error(attaches)
|
|
342
|
+
|
|
343
|
+
response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
|
|
344
|
+
else:
|
|
345
|
+
raise
|
|
222
346
|
|
|
223
|
-
response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
|
|
224
347
|
message = require_payload_item_model(
|
|
225
348
|
response,
|
|
226
349
|
MessagePayloadKey.MESSAGE,
|
|
@@ -243,7 +366,7 @@ class MessageService:
|
|
|
243
366
|
get_chat: bool = False,
|
|
244
367
|
get_messages: bool = True,
|
|
245
368
|
interactive: bool = False,
|
|
246
|
-
) -> list[Message]
|
|
369
|
+
) -> list[Message]:
|
|
247
370
|
frame = ChatHistoryPayload(
|
|
248
371
|
chat_id=chat_id,
|
|
249
372
|
forward=forward,
|
|
@@ -265,7 +388,7 @@ class MessageService:
|
|
|
265
388
|
self.app,
|
|
266
389
|
parse_payload_list(response, MessagePayloadKey.MESSAGES, Message),
|
|
267
390
|
)
|
|
268
|
-
return messages
|
|
391
|
+
return messages
|
|
269
392
|
|
|
270
393
|
async def delete_message(
|
|
271
394
|
self,
|
|
@@ -356,7 +479,7 @@ class MessageService:
|
|
|
356
479
|
async def add_reaction(
|
|
357
480
|
self,
|
|
358
481
|
chat_id: int,
|
|
359
|
-
message_id:
|
|
482
|
+
message_id: int,
|
|
360
483
|
reaction: str,
|
|
361
484
|
) -> ReactionInfo | None:
|
|
362
485
|
logger.info(
|
|
@@ -381,7 +504,7 @@ class MessageService:
|
|
|
381
504
|
async def get_reactions(
|
|
382
505
|
self,
|
|
383
506
|
chat_id: int,
|
|
384
|
-
message_ids: list[
|
|
507
|
+
message_ids: list[int],
|
|
385
508
|
) -> dict[str, ReactionInfo] | None:
|
|
386
509
|
logger.info(
|
|
387
510
|
"getting reactions chat_id=%s message_ids=%s",
|
|
@@ -409,7 +532,7 @@ class MessageService:
|
|
|
409
532
|
async def remove_reaction(
|
|
410
533
|
self,
|
|
411
534
|
chat_id: int,
|
|
412
|
-
message_id:
|
|
535
|
+
message_id: int,
|
|
413
536
|
) -> ReactionInfo | None:
|
|
414
537
|
logger.info(
|
|
415
538
|
"removing reaction chat_id=%s message_id=%s",
|
|
@@ -443,3 +566,17 @@ class MessageService:
|
|
|
443
566
|
|
|
444
567
|
response = await self.app.invoke(Opcode.CHAT_MARK, frame.to_payload())
|
|
445
568
|
return require_payload_model(response, ReadState)
|
|
569
|
+
|
|
570
|
+
async def vote_poll(
|
|
571
|
+
self,
|
|
572
|
+
chat_id: int,
|
|
573
|
+
message_id: int,
|
|
574
|
+
poll_id: int,
|
|
575
|
+
answer_ids: list[int],
|
|
576
|
+
) -> PollState:
|
|
577
|
+
frame = VotePollPayload(
|
|
578
|
+
chat_id=chat_id, message_id=message_id, poll_id=poll_id, answers_ids=answer_ids
|
|
579
|
+
)
|
|
580
|
+
response = await self.app.invoke(Opcode.SEND_VOTE, frame.to_payload())
|
|
581
|
+
|
|
582
|
+
return require_payload_item_model(response, "state", PollState)
|
pymax/api/self/__init__.py
CHANGED
pymax/api/self/enums.py
CHANGED
|
@@ -9,3 +9,12 @@ class SelfPayloadKey(str, Enum):
|
|
|
9
9
|
PROFILE = "profile"
|
|
10
10
|
URL = "url"
|
|
11
11
|
TOKEN = "token"
|
|
12
|
+
HASH = "hash"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PrivacyAccess(str, Enum):
|
|
16
|
+
"""Уровень доступа к данным и действиям аккаунта."""
|
|
17
|
+
|
|
18
|
+
ALL = "ALL"
|
|
19
|
+
CONTACTS = "CONTACTS"
|
|
20
|
+
NOBODY = "_NONE_"
|
pymax/api/self/payloads.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
3
5
|
from pymax.api.models import CamelModel
|
|
4
6
|
|
|
5
|
-
from .enums import AvatarType
|
|
7
|
+
from .enums import AvatarType, PrivacyAccess
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class UploadPayload(CamelModel):
|
|
@@ -39,3 +41,40 @@ class UpdateFolderPayload(CamelModel):
|
|
|
39
41
|
|
|
40
42
|
class DeleteFolderPayload(CamelModel):
|
|
41
43
|
folder_ids: list[str]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PrivacySettingsUpdate(BaseModel):
|
|
47
|
+
"""Изменяемые настройки приватности аккаунта.
|
|
48
|
+
|
|
49
|
+
Все поля необязательны: в запрос попадают только переданные настройки.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
search_by_phone: Кто может найти аккаунт по номеру телефона.
|
|
53
|
+
incoming_calls: Кто может звонить аккаунту.
|
|
54
|
+
chat_invites: Кто может добавлять аккаунт в чаты.
|
|
55
|
+
phone_number_visibility: Кто может видеть номер телефона.
|
|
56
|
+
hide_online_status: Скрывать ли статус присутствия.
|
|
57
|
+
safe_content_only: Показывать только безопасный контент.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
search_by_phone: PrivacyAccess | None = Field(
|
|
61
|
+
default=None, serialization_alias="SEARCH_BY_PHONE"
|
|
62
|
+
)
|
|
63
|
+
incoming_calls: PrivacyAccess | None = Field(default=None, serialization_alias="INCOMING_CALL")
|
|
64
|
+
chat_invites: PrivacyAccess | None = Field(default=None, serialization_alias="CHATS_INVITE")
|
|
65
|
+
phone_number_visibility: PrivacyAccess | None = Field(
|
|
66
|
+
default=None, serialization_alias="PHONE_NUMBER_PRIVACY"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
hide_online_status: bool | None = Field(default=None, serialization_alias="HIDDEN")
|
|
70
|
+
safe_content_only: bool | None = Field(
|
|
71
|
+
default=None, serialization_alias="CONTENT_LEVEL_ACCESS"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ChangeProfileSettings(CamelModel):
|
|
76
|
+
user: PrivacySettingsUpdate
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ChangeProfileSettingsPayload(CamelModel):
|
|
80
|
+
settings: ChangeProfileSettings
|
pymax/api/self/service.py
CHANGED
|
@@ -18,9 +18,12 @@ from pymax.types.domain import FolderList, FolderUpdate, Profile
|
|
|
18
18
|
from .enums import SelfPayloadKey
|
|
19
19
|
from .payloads import (
|
|
20
20
|
ChangeProfilePayload,
|
|
21
|
+
ChangeProfileSettings,
|
|
22
|
+
ChangeProfileSettingsPayload,
|
|
21
23
|
CreateFolderPayload,
|
|
22
24
|
DeleteFolderPayload,
|
|
23
25
|
GetFolderPayload,
|
|
26
|
+
PrivacySettingsUpdate,
|
|
24
27
|
UpdateFolderPayload,
|
|
25
28
|
UploadPayload,
|
|
26
29
|
)
|
|
@@ -150,3 +153,37 @@ class SelfService:
|
|
|
150
153
|
logger.info("logging out")
|
|
151
154
|
await self.app.invoke(Opcode.LOGOUT, {})
|
|
152
155
|
return True
|
|
156
|
+
|
|
157
|
+
def set_presence(self, online: bool) -> None:
|
|
158
|
+
logger.info("setting presence to %s", "online" if online else "offline")
|
|
159
|
+
self.app.config.interactive = online
|
|
160
|
+
|
|
161
|
+
async def change_profile_settings(self, settings: PrivacySettingsUpdate) -> bool:
|
|
162
|
+
logger.info("changing profile settings")
|
|
163
|
+
|
|
164
|
+
frame = ChangeProfileSettingsPayload(settings=ChangeProfileSettings(user=settings))
|
|
165
|
+
|
|
166
|
+
response = await self.app.invoke(
|
|
167
|
+
Opcode.CONFIG,
|
|
168
|
+
frame.to_payload(),
|
|
169
|
+
)
|
|
170
|
+
session = self.app.session
|
|
171
|
+
|
|
172
|
+
if not session:
|
|
173
|
+
logger.warning("no session found, skipping sync update")
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
sync = session.sync.model_copy(
|
|
177
|
+
update={"config_hash": require_payload_item(response, SelfPayloadKey.HASH)}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
updated = session.model_copy(
|
|
181
|
+
update={
|
|
182
|
+
"mt_instance_id": self.app.config.device.mt_instance_id,
|
|
183
|
+
"sync": sync,
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
self.app.session = updated
|
|
187
|
+
await self.app.store.save_session(updated)
|
|
188
|
+
|
|
189
|
+
return True
|
pymax/api/session/service.py
CHANGED
|
@@ -2,8 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
+
from pymax.api.response import require_payload_model
|
|
5
6
|
from pymax.logging import get_logger
|
|
6
7
|
from pymax.protocol import Opcode
|
|
8
|
+
from pymax.types.domain import HandshakeResponse
|
|
7
9
|
|
|
8
10
|
from .enums import DeviceType
|
|
9
11
|
from .payloads import (
|
|
@@ -28,19 +30,18 @@ class SessionService:
|
|
|
28
30
|
mt_instance_id: str,
|
|
29
31
|
user_agent: MobileUserAgentPayload,
|
|
30
32
|
device_id: str,
|
|
31
|
-
) ->
|
|
33
|
+
) -> HandshakeResponse:
|
|
32
34
|
if user_agent.device_type == DeviceType.WEB:
|
|
33
|
-
await self.web_handshake(user_agent, device_id)
|
|
34
|
-
return
|
|
35
|
+
return await self.web_handshake(user_agent, device_id)
|
|
35
36
|
|
|
36
|
-
await self.mobile_handshake(mt_instance_id, user_agent, device_id)
|
|
37
|
+
return await self.mobile_handshake(mt_instance_id, user_agent, device_id)
|
|
37
38
|
|
|
38
39
|
async def mobile_handshake(
|
|
39
40
|
self,
|
|
40
41
|
mt_instance_id: str,
|
|
41
42
|
user_agent: MobileUserAgentPayload,
|
|
42
43
|
device_id: str,
|
|
43
|
-
) ->
|
|
44
|
+
) -> HandshakeResponse:
|
|
44
45
|
logger.debug(
|
|
45
46
|
"mobile handshake mt_instance_id_set=%s device_id=%s app_version=%s",
|
|
46
47
|
bool(mt_instance_id),
|
|
@@ -52,10 +53,17 @@ class SessionService:
|
|
|
52
53
|
user_agent=user_agent,
|
|
53
54
|
device_id=device_id,
|
|
54
55
|
)
|
|
55
|
-
await self.app.invoke(Opcode.SESSION_INIT, frame.to_payload())
|
|
56
|
+
response = await self.app.invoke(Opcode.SESSION_INIT, frame.to_payload())
|
|
56
57
|
logger.info("mobile handshake completed")
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
return require_payload_model(
|
|
60
|
+
response,
|
|
61
|
+
HandshakeResponse,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
async def web_handshake(
|
|
65
|
+
self, user_agent: MobileUserAgentPayload, device_id: str
|
|
66
|
+
) -> HandshakeResponse:
|
|
59
67
|
logger.debug(
|
|
60
68
|
"web handshake device_id=%s app_version=%s browser=%s",
|
|
61
69
|
device_id,
|
|
@@ -66,5 +74,10 @@ class SessionService:
|
|
|
66
74
|
user_agent=user_agent,
|
|
67
75
|
device_id=device_id,
|
|
68
76
|
)
|
|
69
|
-
await self.app.invoke(Opcode.SESSION_INIT, frame.to_payload())
|
|
77
|
+
response = await self.app.invoke(Opcode.SESSION_INIT, frame.to_payload())
|
|
70
78
|
logger.info("web handshake completed")
|
|
79
|
+
|
|
80
|
+
return require_payload_model(
|
|
81
|
+
response,
|
|
82
|
+
HandshakeResponse,
|
|
83
|
+
)
|
pymax/api/uploads/payloads.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, SerializerFunctionWrapHandler, model_serializer
|
|
2
4
|
|
|
3
5
|
from pymax.api.models import CamelModel
|
|
4
6
|
from pymax.types import AttachmentType
|
|
@@ -13,6 +15,40 @@ class VideoAttachPayload(CamelModel):
|
|
|
13
15
|
type: AttachmentType = Field(default=AttachmentType.VIDEO, serialization_alias="_type")
|
|
14
16
|
video_id: int
|
|
15
17
|
token: str
|
|
18
|
+
video_type: int = 0
|
|
19
|
+
thumbhash: bytes | None = None
|
|
20
|
+
duration: int | None = None
|
|
21
|
+
wave: bytes | None = None
|
|
22
|
+
|
|
23
|
+
@model_serializer(mode="wrap")
|
|
24
|
+
def serialize_attachment(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
|
|
25
|
+
payload = handler(self)
|
|
26
|
+
|
|
27
|
+
if self.type == AttachmentType.AUDIO:
|
|
28
|
+
payload.pop("videoId", None)
|
|
29
|
+
payload.pop("videoType", None)
|
|
30
|
+
if not self.token:
|
|
31
|
+
payload.pop("token", None)
|
|
32
|
+
payload["audioId"] = self.video_id
|
|
33
|
+
elif self.video_type == 1:
|
|
34
|
+
if self.token:
|
|
35
|
+
payload.pop("videoId", None)
|
|
36
|
+
else:
|
|
37
|
+
payload.pop("token", None)
|
|
38
|
+
payload["videoId"] = self.video_id
|
|
39
|
+
|
|
40
|
+
return payload
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class VoiceAttachPayload(VideoAttachPayload):
|
|
44
|
+
type: AttachmentType = Field(default=AttachmentType.AUDIO, serialization_alias="_type")
|
|
45
|
+
video_id: int = Field(exclude=True)
|
|
46
|
+
video_type: int = Field(default=0, exclude=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class VideoNoteAttachPayload(VideoAttachPayload):
|
|
50
|
+
video_id: int = Field(exclude=True)
|
|
51
|
+
video_type: int = 1
|
|
16
52
|
|
|
17
53
|
|
|
18
54
|
class AttachFilePayload(CamelModel):
|
|
@@ -22,4 +58,6 @@ class AttachFilePayload(CamelModel):
|
|
|
22
58
|
|
|
23
59
|
class UploadPayload(CamelModel):
|
|
24
60
|
count: int = 1
|
|
61
|
+
type: int = 0
|
|
62
|
+
uploader_type: int = 0
|
|
25
63
|
profile: bool = False
|