maxapi-python 2.1.3__py3-none-any.whl → 2.3.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.3.0.dist-info}/METADATA +3 -11
- {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/RECORD +64 -59
- 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/payloads.py +6 -0
- pymax/api/chats/service.py +52 -47
- pymax/api/messages/enums.py +1 -0
- pymax/api/messages/payloads.py +16 -1
- pymax/api/messages/service.py +78 -34
- 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/payloads.py +22 -0
- pymax/api/users/service.py +22 -17
- pymax/app.py +28 -6
- pymax/auth/qr.py +3 -9
- pymax/auth/sms.py +23 -11
- pymax/base.py +86 -4
- 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/__init__.py +12 -1
- pymax/dispatch/dispatcher.py +170 -34
- pymax/dispatch/enums.py +5 -0
- pymax/dispatch/mapping.py +34 -11
- pymax/dispatch/resolvers.py +18 -0
- pymax/dispatch/router.py +120 -4
- pymax/formatting/markdown.py +22 -13
- pymax/infra/chat.py +33 -0
- pymax/infra/message.py +69 -2
- pymax/infra/user.py +12 -1
- 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 +19 -24
- pymax/telemetry/navigation.py +1 -3
- pymax/telemetry/service.py +5 -17
- pymax/transport/tcp.py +1 -3
- pymax/types/domain/__init__.py +1 -1
- pymax/types/domain/attachments/unknown.py +1 -3
- pymax/types/domain/auth.py +24 -2
- pymax/types/domain/chat.py +58 -1
- pymax/types/domain/message.py +28 -2
- pymax/types/domain/presence.py +3 -3
- pymax/types/domain/sync.py +5 -21
- pymax/types/domain/user.py +8 -0
- 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.3.0.dist-info}/WHEEL +0 -0
- {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/licenses/LICENSE +0 -0
pymax/api/chats/service.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import time
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
|
+
from pymax.api.binding import bind_api_model
|
|
6
7
|
from pymax.api.response import (
|
|
7
8
|
parse_payload_item_model,
|
|
8
9
|
parse_payload_list,
|
|
@@ -22,6 +23,7 @@ from .payloads import (
|
|
|
22
23
|
CreateGroupAttach,
|
|
23
24
|
CreateGroupMessage,
|
|
24
25
|
CreateGroupPayload,
|
|
26
|
+
DeleteChatPayload,
|
|
25
27
|
FetchChatsPayload,
|
|
26
28
|
FetchJoinRequests,
|
|
27
29
|
GetChatInfoPayload,
|
|
@@ -46,7 +48,7 @@ class ChatService:
|
|
|
46
48
|
self.app = app
|
|
47
49
|
|
|
48
50
|
def _bind_chat(self, chat: Chat) -> Chat:
|
|
49
|
-
return
|
|
51
|
+
return bind_api_model(self.app, chat)
|
|
50
52
|
|
|
51
53
|
def _cache_chat(self, chat: Chat) -> Chat:
|
|
52
54
|
chat = self._bind_chat(chat)
|
|
@@ -72,15 +74,19 @@ class ChatService:
|
|
|
72
74
|
if self.app.chats is None:
|
|
73
75
|
return
|
|
74
76
|
|
|
75
|
-
self.app.chats = [
|
|
76
|
-
chat for chat in self.app.chats if chat.id != chat_id
|
|
77
|
-
]
|
|
77
|
+
self.app.chats = [chat for chat in self.app.chats if chat.id != chat_id]
|
|
78
78
|
|
|
79
79
|
@staticmethod
|
|
80
80
|
def _process_chat_join_link(link: str) -> str | None:
|
|
81
81
|
idx = link.find(ChatLinkPrefix.JOIN)
|
|
82
82
|
return link[idx:] if idx != -1 else None
|
|
83
83
|
|
|
84
|
+
async def _join_chat(self, link: str) -> Chat:
|
|
85
|
+
frame = JoinChatPayload(link=link)
|
|
86
|
+
response = await self.app.invoke(Opcode.CHAT_JOIN, frame.to_payload())
|
|
87
|
+
chat = require_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
|
|
88
|
+
return self._cache_chat(chat)
|
|
89
|
+
|
|
84
90
|
async def create_group(
|
|
85
91
|
self,
|
|
86
92
|
name: str,
|
|
@@ -112,8 +118,9 @@ class ChatService:
|
|
|
112
118
|
return None
|
|
113
119
|
|
|
114
120
|
chat = self._cache_chat(chat)
|
|
115
|
-
message =
|
|
116
|
-
self.app
|
|
121
|
+
message = bind_api_model(
|
|
122
|
+
self.app,
|
|
123
|
+
require_payload_model(response, Message),
|
|
117
124
|
)
|
|
118
125
|
return chat, message
|
|
119
126
|
|
|
@@ -145,9 +152,7 @@ class ChatService:
|
|
|
145
152
|
user_ids: list[int],
|
|
146
153
|
show_history: bool = True,
|
|
147
154
|
) -> Chat | None:
|
|
148
|
-
return await self.invite_users_to_group(
|
|
149
|
-
chat_id, user_ids, show_history
|
|
150
|
-
)
|
|
155
|
+
return await self.invite_users_to_group(chat_id, user_ids, show_history)
|
|
151
156
|
|
|
152
157
|
async def remove_users_from_group(
|
|
153
158
|
self,
|
|
@@ -191,9 +196,7 @@ class ChatService:
|
|
|
191
196
|
),
|
|
192
197
|
)
|
|
193
198
|
|
|
194
|
-
response = await self.app.invoke(
|
|
195
|
-
Opcode.CHAT_UPDATE, frame.to_payload()
|
|
196
|
-
)
|
|
199
|
+
response = await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload())
|
|
197
200
|
chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
|
|
198
201
|
if chat:
|
|
199
202
|
self._cache_chat(chat)
|
|
@@ -210,9 +213,7 @@ class ChatService:
|
|
|
210
213
|
description=description,
|
|
211
214
|
)
|
|
212
215
|
|
|
213
|
-
response = await self.app.invoke(
|
|
214
|
-
Opcode.CHAT_UPDATE, frame.to_payload()
|
|
215
|
-
)
|
|
216
|
+
response = await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload())
|
|
216
217
|
chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
|
|
217
218
|
if chat:
|
|
218
219
|
self._cache_chat(chat)
|
|
@@ -222,10 +223,12 @@ class ChatService:
|
|
|
222
223
|
if proceed_link is None:
|
|
223
224
|
raise ValueError("Invalid group link")
|
|
224
225
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
return await self._join_chat(proceed_link)
|
|
227
|
+
|
|
228
|
+
async def join_channel(self, link: str) -> Chat:
|
|
229
|
+
proceed_link = self._process_chat_join_link(link)
|
|
230
|
+
|
|
231
|
+
return await self._join_chat(proceed_link or link)
|
|
229
232
|
|
|
230
233
|
async def resolve_group_by_link(self, link: str) -> Chat | None:
|
|
231
234
|
proceed_link = self._process_chat_join_link(link)
|
|
@@ -242,9 +245,7 @@ class ChatService:
|
|
|
242
245
|
|
|
243
246
|
async def rework_invite_link(self, chat_id: int) -> Chat:
|
|
244
247
|
frame = ReworkInviteLinkPayload(chat_id=chat_id)
|
|
245
|
-
response = await self.app.invoke(
|
|
246
|
-
Opcode.CHAT_UPDATE, frame.to_payload()
|
|
247
|
-
)
|
|
248
|
+
response = await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload())
|
|
248
249
|
chat = require_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
|
|
249
250
|
return self._cache_chat(chat)
|
|
250
251
|
|
|
@@ -254,18 +255,12 @@ class ChatService:
|
|
|
254
255
|
for chat_id in chat_ids
|
|
255
256
|
if (chat := self._get_cached_chat(chat_id)) is not None
|
|
256
257
|
}
|
|
257
|
-
missed_chat_ids = [
|
|
258
|
-
chat_id for chat_id in chat_ids if chat_id not in cached
|
|
259
|
-
]
|
|
258
|
+
missed_chat_ids = [chat_id for chat_id in chat_ids if chat_id not in cached]
|
|
260
259
|
|
|
261
260
|
if missed_chat_ids:
|
|
262
261
|
frame = GetChatInfoPayload(chat_ids=missed_chat_ids)
|
|
263
|
-
response = await self.app.invoke(
|
|
264
|
-
|
|
265
|
-
)
|
|
266
|
-
for chat in parse_payload_list(
|
|
267
|
-
response, ChatPayloadKey.CHATS, Chat
|
|
268
|
-
):
|
|
262
|
+
response = await self.app.invoke(Opcode.CHAT_INFO, frame.to_payload())
|
|
263
|
+
for chat in parse_payload_list(response, ChatPayloadKey.CHATS, Chat):
|
|
269
264
|
chat = self._cache_chat(chat)
|
|
270
265
|
cached[chat.id] = chat
|
|
271
266
|
|
|
@@ -292,22 +287,19 @@ class ChatService:
|
|
|
292
287
|
|
|
293
288
|
chats = [
|
|
294
289
|
self._cache_chat(chat)
|
|
295
|
-
for chat in parse_payload_list(
|
|
296
|
-
response, ChatPayloadKey.CHATS, Chat
|
|
297
|
-
)
|
|
290
|
+
for chat in parse_payload_list(response, ChatPayloadKey.CHATS, Chat)
|
|
298
291
|
]
|
|
299
292
|
return chats
|
|
300
293
|
|
|
301
|
-
async def get_join_requests(
|
|
302
|
-
self, chat_id: int, count: int = 100
|
|
303
|
-
) -> list[Member]:
|
|
294
|
+
async def get_join_requests(self, chat_id: int, count: int = 100) -> list[Member]:
|
|
304
295
|
frame = FetchJoinRequests(chat_id=chat_id, count=count)
|
|
305
296
|
|
|
306
|
-
response = await self.app.invoke(
|
|
307
|
-
Opcode.CHAT_MEMBERS, frame.to_payload()
|
|
308
|
-
)
|
|
297
|
+
response = await self.app.invoke(Opcode.CHAT_MEMBERS, frame.to_payload())
|
|
309
298
|
|
|
310
|
-
return
|
|
299
|
+
return bind_api_model(
|
|
300
|
+
self.app,
|
|
301
|
+
parse_payload_list(response, ChatPayloadKey.MEMBERS, Member),
|
|
302
|
+
)
|
|
311
303
|
|
|
312
304
|
async def confirm_join_requests(
|
|
313
305
|
self,
|
|
@@ -322,9 +314,7 @@ class ChatService:
|
|
|
322
314
|
operation=ChatMemberOperation.ADD,
|
|
323
315
|
)
|
|
324
316
|
|
|
325
|
-
response = await self.app.invoke(
|
|
326
|
-
Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload()
|
|
327
|
-
)
|
|
317
|
+
response = await self.app.invoke(Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload())
|
|
328
318
|
|
|
329
319
|
chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
|
|
330
320
|
if chat:
|
|
@@ -356,9 +346,7 @@ class ChatService:
|
|
|
356
346
|
operation=ChatMemberOperation.REMOVE,
|
|
357
347
|
)
|
|
358
348
|
|
|
359
|
-
response = await self.app.invoke(
|
|
360
|
-
Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload()
|
|
361
|
-
)
|
|
349
|
+
response = await self.app.invoke(Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload())
|
|
362
350
|
|
|
363
351
|
chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
|
|
364
352
|
if chat:
|
|
@@ -375,3 +363,20 @@ class ChatService:
|
|
|
375
363
|
chat_id=chat_id,
|
|
376
364
|
user_ids=[user_id],
|
|
377
365
|
)
|
|
366
|
+
|
|
367
|
+
async def delete_chat(
|
|
368
|
+
self,
|
|
369
|
+
chat_id: int,
|
|
370
|
+
last_event_time: int | None = None,
|
|
371
|
+
for_all: bool = True,
|
|
372
|
+
) -> None:
|
|
373
|
+
frame = DeleteChatPayload(
|
|
374
|
+
chat_id=chat_id,
|
|
375
|
+
last_event_time=(
|
|
376
|
+
last_event_time if last_event_time is not None else int(time.time() * 1000)
|
|
377
|
+
),
|
|
378
|
+
for_all=for_all,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
await self.app.invoke(Opcode.CHAT_DELETE, frame.to_payload())
|
|
382
|
+
self._remove_cached_chat(chat_id)
|
pymax/api/messages/enums.py
CHANGED
pymax/api/messages/payloads.py
CHANGED
|
@@ -12,6 +12,21 @@ from pymax.api.uploads.payloads import (
|
|
|
12
12
|
from .enums import ItemType, ReadAction
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
class GetMessagesPayload(CamelModel):
|
|
16
|
+
chat_id: int
|
|
17
|
+
message_ids: list[int]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EditMessagePayload(CamelModel):
|
|
21
|
+
chat_id: int
|
|
22
|
+
message_id: int
|
|
23
|
+
text: str
|
|
24
|
+
elements: list[Any]
|
|
25
|
+
attachments: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload] = Field(
|
|
26
|
+
default_factory=list
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
15
30
|
class ReplyLink(CamelModel):
|
|
16
31
|
type: str = "REPLY" # TODO: enum?
|
|
17
32
|
message_id: int
|
|
@@ -92,5 +107,5 @@ class RemoveReactionPayload(CamelModel):
|
|
|
92
107
|
class ReadMessagesPayload(CamelModel):
|
|
93
108
|
type: ReadAction
|
|
94
109
|
chat_id: int
|
|
95
|
-
message_id: str
|
|
110
|
+
message_id: str | int # Сокет просит int а вс str
|
|
96
111
|
mark: int
|
pymax/api/messages/service.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
+
from collections.abc import Sequence
|
|
4
5
|
from typing import TYPE_CHECKING, TypeAlias
|
|
5
6
|
|
|
7
|
+
from pymax.api.binding import bind_api_model, bind_api_models
|
|
6
8
|
from pymax.api.response import (
|
|
7
9
|
parse_payload_list,
|
|
8
10
|
parse_payload_model,
|
|
9
11
|
payload_item,
|
|
12
|
+
require_payload_item_model,
|
|
10
13
|
require_payload_model,
|
|
11
14
|
)
|
|
12
15
|
from pymax.api.uploads.payloads import (
|
|
@@ -32,7 +35,9 @@ from .payloads import (
|
|
|
32
35
|
AddReactionPayload,
|
|
33
36
|
ChatHistoryPayload,
|
|
34
37
|
DeleteMessagePayload,
|
|
38
|
+
EditMessagePayload,
|
|
35
39
|
GetFilePayload,
|
|
40
|
+
GetMessagesPayload,
|
|
36
41
|
GetReactionsPayload,
|
|
37
42
|
GetVideoPayload,
|
|
38
43
|
PinMessagePayload,
|
|
@@ -48,7 +53,7 @@ if TYPE_CHECKING:
|
|
|
48
53
|
from pymax.app import App
|
|
49
54
|
|
|
50
55
|
SendAttachment: TypeAlias = Photo | File | Video
|
|
51
|
-
SendAttachments: TypeAlias =
|
|
56
|
+
SendAttachments: TypeAlias = Sequence[SendAttachment] | None
|
|
52
57
|
|
|
53
58
|
logger = get_logger(__name__)
|
|
54
59
|
|
|
@@ -69,17 +74,13 @@ class MessageService:
|
|
|
69
74
|
async def _upload_attachments(
|
|
70
75
|
self, attachments: SendAttachments
|
|
71
76
|
) -> list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload]:
|
|
72
|
-
result: list[
|
|
73
|
-
AttachPhotoPayload | VideoAttachPayload | AttachFilePayload
|
|
74
|
-
] = []
|
|
77
|
+
result: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload] = []
|
|
75
78
|
if not attachments:
|
|
76
79
|
return result
|
|
77
80
|
|
|
78
81
|
for attachment in attachments:
|
|
79
82
|
if isinstance(attachment, Photo):
|
|
80
|
-
upload_result = await self.app.api.uploads.upload_photo(
|
|
81
|
-
attachment
|
|
82
|
-
)
|
|
83
|
+
upload_result = await self.app.api.uploads.upload_photo(attachment)
|
|
83
84
|
if not upload_result:
|
|
84
85
|
logger.error("Photo uploading failed")
|
|
85
86
|
raise UploadError("Photo uploading failed")
|
|
@@ -87,9 +88,7 @@ class MessageService:
|
|
|
87
88
|
result.append(upload_result)
|
|
88
89
|
|
|
89
90
|
elif isinstance(attachment, Video):
|
|
90
|
-
upload_result = await self.app.api.uploads.upload_video(
|
|
91
|
-
attachment
|
|
92
|
-
)
|
|
91
|
+
upload_result = await self.app.api.uploads.upload_video(attachment)
|
|
93
92
|
if not upload_result:
|
|
94
93
|
logger.error("Video uploading failed")
|
|
95
94
|
raise UploadError("Video uploading failed")
|
|
@@ -97,9 +96,7 @@ class MessageService:
|
|
|
97
96
|
result.append(upload_result)
|
|
98
97
|
|
|
99
98
|
elif isinstance(attachment, File):
|
|
100
|
-
upload_result = await self.app.api.uploads.upload_file(
|
|
101
|
-
attachment
|
|
102
|
-
)
|
|
99
|
+
upload_result = await self.app.api.uploads.upload_file(attachment)
|
|
103
100
|
if not upload_result:
|
|
104
101
|
logger.error("File uploading failed")
|
|
105
102
|
raise UploadError("File uploading failed")
|
|
@@ -117,9 +114,7 @@ class MessageService:
|
|
|
117
114
|
*,
|
|
118
115
|
notify: bool = True,
|
|
119
116
|
) -> Message | None:
|
|
120
|
-
logger.info(
|
|
121
|
-
"sending message chat_id=%s text_len=%s", chat_id, len(text)
|
|
122
|
-
)
|
|
117
|
+
logger.info("sending message chat_id=%s text_len=%s", chat_id, len(text))
|
|
123
118
|
|
|
124
119
|
clean_text, elements = Formatter.format_markdown(text)
|
|
125
120
|
|
|
@@ -137,10 +132,66 @@ class MessageService:
|
|
|
137
132
|
|
|
138
133
|
response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
|
|
139
134
|
|
|
140
|
-
message =
|
|
135
|
+
message = bind_api_model(
|
|
136
|
+
self.app,
|
|
137
|
+
require_payload_model(response, Message),
|
|
138
|
+
)
|
|
141
139
|
logger.info("message sent chat_id=%s", chat_id)
|
|
142
140
|
return message
|
|
143
141
|
|
|
142
|
+
async def get_messages(
|
|
143
|
+
self,
|
|
144
|
+
chat_id: int,
|
|
145
|
+
message_ids: list[int],
|
|
146
|
+
) -> list[Message]:
|
|
147
|
+
frame = GetMessagesPayload(
|
|
148
|
+
chat_id=chat_id,
|
|
149
|
+
message_ids=message_ids,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
response = await self.app.invoke(Opcode.MSG_GET, frame.to_payload())
|
|
153
|
+
messages = parse_payload_list(response, MessagePayloadKey.MESSAGES, Message)
|
|
154
|
+
for message in messages:
|
|
155
|
+
if message.chat_id is None:
|
|
156
|
+
message.chat_id = chat_id
|
|
157
|
+
|
|
158
|
+
return bind_api_models(self.app, messages)
|
|
159
|
+
|
|
160
|
+
async def get_message(
|
|
161
|
+
self,
|
|
162
|
+
chat_id: int,
|
|
163
|
+
message_id: int,
|
|
164
|
+
) -> Message | None:
|
|
165
|
+
messages = await self.get_messages(chat_id, [message_id])
|
|
166
|
+
return messages[0] if messages else None
|
|
167
|
+
|
|
168
|
+
async def edit_message(
|
|
169
|
+
self,
|
|
170
|
+
chat_id: int,
|
|
171
|
+
message_id: int,
|
|
172
|
+
text: str,
|
|
173
|
+
attachments: SendAttachments = None,
|
|
174
|
+
) -> Message:
|
|
175
|
+
clean_text, elements = Formatter.format_markdown(text)
|
|
176
|
+
frame = EditMessagePayload(
|
|
177
|
+
chat_id=chat_id,
|
|
178
|
+
message_id=message_id,
|
|
179
|
+
text=clean_text,
|
|
180
|
+
elements=elements,
|
|
181
|
+
attachments=await self._upload_attachments(attachments),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
|
|
185
|
+
message = require_payload_item_model(
|
|
186
|
+
response,
|
|
187
|
+
MessagePayloadKey.MESSAGE,
|
|
188
|
+
Message,
|
|
189
|
+
)
|
|
190
|
+
if message.chat_id is None:
|
|
191
|
+
message.chat_id = chat_id
|
|
192
|
+
|
|
193
|
+
return bind_api_model(self.app, message)
|
|
194
|
+
|
|
144
195
|
async def fetch_history(
|
|
145
196
|
self,
|
|
146
197
|
chat_id: int,
|
|
@@ -171,10 +222,11 @@ class MessageService:
|
|
|
171
222
|
Opcode.CHAT_HISTORY,
|
|
172
223
|
payload=frame.to_payload(),
|
|
173
224
|
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
225
|
+
messages = bind_api_models(
|
|
226
|
+
self.app,
|
|
227
|
+
parse_payload_list(response, MessagePayloadKey.MESSAGES, Message),
|
|
177
228
|
)
|
|
229
|
+
return messages or None
|
|
178
230
|
|
|
179
231
|
async def delete_message(
|
|
180
232
|
self,
|
|
@@ -195,9 +247,7 @@ class MessageService:
|
|
|
195
247
|
)
|
|
196
248
|
|
|
197
249
|
await self.app.invoke(Opcode.MSG_DELETE, frame.to_payload())
|
|
198
|
-
logger.info(
|
|
199
|
-
"messages deleted chat_id=%s count=%s", chat_id, len(message_ids)
|
|
200
|
-
)
|
|
250
|
+
logger.info("messages deleted chat_id=%s count=%s", chat_id, len(message_ids))
|
|
201
251
|
return True
|
|
202
252
|
|
|
203
253
|
async def pin_message(
|
|
@@ -219,9 +269,7 @@ class MessageService:
|
|
|
219
269
|
)
|
|
220
270
|
|
|
221
271
|
await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload())
|
|
222
|
-
logger.info(
|
|
223
|
-
"message pinned chat_id=%s message_id=%s", chat_id, message_id
|
|
224
|
-
)
|
|
272
|
+
logger.info("message pinned chat_id=%s message_id=%s", chat_id, message_id)
|
|
225
273
|
return True
|
|
226
274
|
|
|
227
275
|
async def get_video_by_id(
|
|
@@ -263,9 +311,7 @@ class MessageService:
|
|
|
263
311
|
file_id=file_id,
|
|
264
312
|
)
|
|
265
313
|
|
|
266
|
-
response = await self.app.invoke(
|
|
267
|
-
Opcode.FILE_DOWNLOAD, frame.to_payload()
|
|
268
|
-
)
|
|
314
|
+
response = await self.app.invoke(Opcode.FILE_DOWNLOAD, frame.to_payload())
|
|
269
315
|
return parse_payload_model(response, FileRequest)
|
|
270
316
|
|
|
271
317
|
async def add_reaction(
|
|
@@ -286,9 +332,7 @@ class MessageService:
|
|
|
286
332
|
reaction=ReactionInfoPayload(id=reaction),
|
|
287
333
|
)
|
|
288
334
|
|
|
289
|
-
response = await self.app.invoke(
|
|
290
|
-
Opcode.MSG_REACTION, frame.to_payload()
|
|
291
|
-
)
|
|
335
|
+
response = await self.app.invoke(Opcode.MSG_REACTION, frame.to_payload())
|
|
292
336
|
reaction_info = payload_item(response, MessagePayloadKey.REACTION_INFO)
|
|
293
337
|
if reaction_info:
|
|
294
338
|
return ReactionInfo.model_validate(reaction_info)
|
|
@@ -345,7 +389,7 @@ class MessageService:
|
|
|
345
389
|
|
|
346
390
|
return None
|
|
347
391
|
|
|
348
|
-
async def read_message(self, message_id: int, chat_id: int) -> ReadState:
|
|
392
|
+
async def read_message(self, message_id: int | str, chat_id: int) -> ReadState:
|
|
349
393
|
logger.info(
|
|
350
394
|
"marking message as read chat_id=%s message_id=%s",
|
|
351
395
|
chat_id,
|
|
@@ -354,7 +398,7 @@ class MessageService:
|
|
|
354
398
|
frame = ReadMessagesPayload(
|
|
355
399
|
type=ReadAction.READ_MESSAGE,
|
|
356
400
|
chat_id=chat_id,
|
|
357
|
-
message_id=
|
|
401
|
+
message_id=message_id,
|
|
358
402
|
mark=int(time.time() * 1000),
|
|
359
403
|
)
|
|
360
404
|
|
pymax/api/models.py
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
from pydantic import BaseModel
|
|
1
|
+
from pydantic import BaseModel, ConfigDict
|
|
2
2
|
from pydantic.alias_generators import to_camel
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class CamelModel(BaseModel):
|
|
6
|
-
model_config =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"arbitrary_types_allowed": True,
|
|
10
|
-
}
|
|
6
|
+
model_config = ConfigDict(
|
|
7
|
+
alias_generator=to_camel, populate_by_name=True, arbitrary_types_allowed=True
|
|
8
|
+
)
|
|
11
9
|
|
|
12
10
|
def to_payload(self) -> dict:
|
|
13
11
|
return self.model_dump(by_alias=True, exclude_none=True)
|
pymax/api/response.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import Any, TypeVar, overload
|
|
2
|
+
from typing import Any, TypeVar, cast, overload
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel
|
|
5
5
|
|
|
@@ -60,7 +60,7 @@ def payload_item(
|
|
|
60
60
|
if validation_type is None:
|
|
61
61
|
return data
|
|
62
62
|
|
|
63
|
-
return validation_type(data)
|
|
63
|
+
return cast(Any, validation_type)(data)
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
def require_payload_item(
|
pymax/api/self/service.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
from uuid import uuid4
|
|
5
5
|
|
|
6
|
+
from pymax.api.binding import bind_api_model
|
|
6
7
|
from pymax.api.response import (
|
|
7
8
|
payload_item,
|
|
8
9
|
require_payload_item,
|
|
@@ -38,9 +39,7 @@ class SelfService:
|
|
|
38
39
|
async def request_profile_photo_upload_url(self) -> str:
|
|
39
40
|
logger.info("requesting profile photo upload url")
|
|
40
41
|
frame = UploadPayload(profile=True)
|
|
41
|
-
response = await self.app.invoke(
|
|
42
|
-
Opcode.PHOTO_UPLOAD, frame.to_payload()
|
|
43
|
-
)
|
|
42
|
+
response = await self.app.invoke(Opcode.PHOTO_UPLOAD, frame.to_payload())
|
|
44
43
|
return str(require_payload_item(response, SelfPayloadKey.URL))
|
|
45
44
|
|
|
46
45
|
async def change_profile(
|
|
@@ -53,12 +52,11 @@ class SelfService:
|
|
|
53
52
|
photo_token: str | None = None,
|
|
54
53
|
) -> bool:
|
|
55
54
|
if photo is not None:
|
|
56
|
-
attach = await self.app.api.uploads.upload_photo(
|
|
57
|
-
photo, profile=True
|
|
58
|
-
)
|
|
55
|
+
attach = await self.app.api.uploads.upload_photo(photo, profile=True)
|
|
59
56
|
if photo_token:
|
|
60
57
|
logger.warning(
|
|
61
|
-
"photo_token argument was provided but will be overridden by
|
|
58
|
+
"photo_token argument was provided but will be overridden by "
|
|
59
|
+
"the uploaded photo token"
|
|
62
60
|
)
|
|
63
61
|
|
|
64
62
|
photo_token = attach.photo_token
|
|
@@ -70,10 +68,13 @@ class SelfService:
|
|
|
70
68
|
photo_token=photo_token,
|
|
71
69
|
)
|
|
72
70
|
response = await self.app.invoke(Opcode.PROFILE, frame.to_payload())
|
|
73
|
-
profile =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
profile = bind_api_model(
|
|
72
|
+
self.app,
|
|
73
|
+
require_payload_item_model(
|
|
74
|
+
response,
|
|
75
|
+
SelfPayloadKey.PROFILE,
|
|
76
|
+
Profile,
|
|
77
|
+
),
|
|
77
78
|
)
|
|
78
79
|
self.app.me = profile
|
|
79
80
|
self.app.users[profile.contact.id] = profile.contact
|
|
@@ -92,17 +93,13 @@ class SelfService:
|
|
|
92
93
|
include=chat_include,
|
|
93
94
|
filters=filters or [],
|
|
94
95
|
)
|
|
95
|
-
response = await self.app.invoke(
|
|
96
|
-
Opcode.FOLDERS_UPDATE, frame.to_payload()
|
|
97
|
-
)
|
|
96
|
+
response = await self.app.invoke(Opcode.FOLDERS_UPDATE, frame.to_payload())
|
|
98
97
|
return require_payload_model(response, FolderUpdate)
|
|
99
98
|
|
|
100
99
|
async def get_folders(self, folder_sync: int = 0) -> FolderList:
|
|
101
100
|
logger.info("fetching folders")
|
|
102
101
|
frame = GetFolderPayload(folder_sync=folder_sync)
|
|
103
|
-
response = await self.app.invoke(
|
|
104
|
-
Opcode.FOLDERS_GET, frame.to_payload()
|
|
105
|
-
)
|
|
102
|
+
response = await self.app.invoke(Opcode.FOLDERS_GET, frame.to_payload())
|
|
106
103
|
return require_payload_model(response, FolderList)
|
|
107
104
|
|
|
108
105
|
async def update_folder(
|
|
@@ -121,17 +118,13 @@ class SelfService:
|
|
|
121
118
|
filters=filters or [],
|
|
122
119
|
options=options or [],
|
|
123
120
|
)
|
|
124
|
-
response = await self.app.invoke(
|
|
125
|
-
Opcode.FOLDERS_UPDATE, frame.to_payload()
|
|
126
|
-
)
|
|
121
|
+
response = await self.app.invoke(Opcode.FOLDERS_UPDATE, frame.to_payload())
|
|
127
122
|
return require_payload_model(response, FolderUpdate)
|
|
128
123
|
|
|
129
124
|
async def delete_folder(self, folder_id: str) -> FolderUpdate:
|
|
130
125
|
logger.info("deleting folder")
|
|
131
126
|
frame = DeleteFolderPayload(folder_ids=[folder_id])
|
|
132
|
-
response = await self.app.invoke(
|
|
133
|
-
Opcode.FOLDERS_DELETE, frame.to_payload()
|
|
134
|
-
)
|
|
127
|
+
response = await self.app.invoke(Opcode.FOLDERS_DELETE, frame.to_payload())
|
|
135
128
|
return require_payload_model(response, FolderUpdate)
|
|
136
129
|
|
|
137
130
|
async def close_all_sessions(self) -> bool:
|
|
@@ -145,9 +138,7 @@ class SelfService:
|
|
|
145
138
|
token = payload_item(response, SelfPayloadKey.TOKEN, str)
|
|
146
139
|
|
|
147
140
|
if not token:
|
|
148
|
-
logger.warning(
|
|
149
|
-
"no token received after closing sessions, skipping token update"
|
|
150
|
-
)
|
|
141
|
+
logger.warning("no token received after closing sessions, skipping token update")
|
|
151
142
|
return False
|
|
152
143
|
|
|
153
144
|
await self.app.store.update_token(self.app.session.token, token)
|
pymax/api/session/payloads.py
CHANGED
|
@@ -52,17 +52,10 @@ class MobileUserAgentPayload(CamelModel):
|
|
|
52
52
|
by_alias=True,
|
|
53
53
|
exclude_none=True,
|
|
54
54
|
)
|
|
55
|
-
if
|
|
56
|
-
self.device_type == DeviceType.WEB
|
|
57
|
-
and "headerUserAgent" not in payload
|
|
58
|
-
):
|
|
55
|
+
if self.device_type == DeviceType.WEB and "headerUserAgent" not in payload:
|
|
59
56
|
payload["headerUserAgent"] = DEFAULT_WEB_HEADER_USER_AGENT
|
|
60
57
|
|
|
61
|
-
return {
|
|
62
|
-
alias: payload[alias]
|
|
63
|
-
for alias in WEB_USER_AGENT_ALIASES
|
|
64
|
-
if alias in payload
|
|
65
|
-
}
|
|
58
|
+
return {alias: payload[alias] for alias in WEB_USER_AGENT_ALIASES if alias in payload}
|
|
66
59
|
|
|
67
60
|
|
|
68
61
|
class MobileHandshakePayload(CamelModel):
|
pymax/api/session/service.py
CHANGED
|
@@ -55,9 +55,7 @@ class SessionService:
|
|
|
55
55
|
await self.app.invoke(Opcode.SESSION_INIT, frame.to_payload())
|
|
56
56
|
logger.info("mobile handshake completed")
|
|
57
57
|
|
|
58
|
-
async def web_handshake(
|
|
59
|
-
self, user_agent: MobileUserAgentPayload, device_id: str
|
|
60
|
-
) -> None:
|
|
58
|
+
async def web_handshake(self, user_agent: MobileUserAgentPayload, device_id: str) -> None:
|
|
61
59
|
logger.debug(
|
|
62
60
|
"web handshake device_id=%s app_version=%s browser=%s",
|
|
63
61
|
device_id,
|
pymax/api/uploads/payloads.py
CHANGED
|
@@ -5,24 +5,18 @@ from pymax.types import AttachmentType
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class AttachPhotoPayload(CamelModel):
|
|
8
|
-
type: AttachmentType = Field(
|
|
9
|
-
default=AttachmentType.PHOTO, serialization_alias="_type"
|
|
10
|
-
)
|
|
8
|
+
type: AttachmentType = Field(default=AttachmentType.PHOTO, serialization_alias="_type")
|
|
11
9
|
photo_token: str
|
|
12
10
|
|
|
13
11
|
|
|
14
12
|
class VideoAttachPayload(CamelModel):
|
|
15
|
-
type: AttachmentType = Field(
|
|
16
|
-
default=AttachmentType.VIDEO, serialization_alias="_type"
|
|
17
|
-
)
|
|
13
|
+
type: AttachmentType = Field(default=AttachmentType.VIDEO, serialization_alias="_type")
|
|
18
14
|
video_id: int
|
|
19
15
|
token: str
|
|
20
16
|
|
|
21
17
|
|
|
22
18
|
class AttachFilePayload(CamelModel):
|
|
23
|
-
type: AttachmentType = Field(
|
|
24
|
-
default=AttachmentType.FILE, serialization_alias="_type"
|
|
25
|
-
)
|
|
19
|
+
type: AttachmentType = Field(default=AttachmentType.FILE, serialization_alias="_type")
|
|
26
20
|
file_id: int
|
|
27
21
|
|
|
28
22
|
|