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/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,
|
|
@@ -46,7 +47,7 @@ class ChatService:
|
|
|
46
47
|
self.app = app
|
|
47
48
|
|
|
48
49
|
def _bind_chat(self, chat: Chat) -> Chat:
|
|
49
|
-
return
|
|
50
|
+
return bind_api_model(self.app, chat)
|
|
50
51
|
|
|
51
52
|
def _cache_chat(self, chat: Chat) -> Chat:
|
|
52
53
|
chat = self._bind_chat(chat)
|
|
@@ -72,15 +73,19 @@ class ChatService:
|
|
|
72
73
|
if self.app.chats is None:
|
|
73
74
|
return
|
|
74
75
|
|
|
75
|
-
self.app.chats = [
|
|
76
|
-
chat for chat in self.app.chats if chat.id != chat_id
|
|
77
|
-
]
|
|
76
|
+
self.app.chats = [chat for chat in self.app.chats if chat.id != chat_id]
|
|
78
77
|
|
|
79
78
|
@staticmethod
|
|
80
79
|
def _process_chat_join_link(link: str) -> str | None:
|
|
81
80
|
idx = link.find(ChatLinkPrefix.JOIN)
|
|
82
81
|
return link[idx:] if idx != -1 else None
|
|
83
82
|
|
|
83
|
+
async def _join_chat(self, link: str) -> Chat:
|
|
84
|
+
frame = JoinChatPayload(link=link)
|
|
85
|
+
response = await self.app.invoke(Opcode.CHAT_JOIN, frame.to_payload())
|
|
86
|
+
chat = require_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
|
|
87
|
+
return self._cache_chat(chat)
|
|
88
|
+
|
|
84
89
|
async def create_group(
|
|
85
90
|
self,
|
|
86
91
|
name: str,
|
|
@@ -112,8 +117,9 @@ class ChatService:
|
|
|
112
117
|
return None
|
|
113
118
|
|
|
114
119
|
chat = self._cache_chat(chat)
|
|
115
|
-
message =
|
|
116
|
-
self.app
|
|
120
|
+
message = bind_api_model(
|
|
121
|
+
self.app,
|
|
122
|
+
require_payload_model(response, Message),
|
|
117
123
|
)
|
|
118
124
|
return chat, message
|
|
119
125
|
|
|
@@ -145,9 +151,7 @@ class ChatService:
|
|
|
145
151
|
user_ids: list[int],
|
|
146
152
|
show_history: bool = True,
|
|
147
153
|
) -> Chat | None:
|
|
148
|
-
return await self.invite_users_to_group(
|
|
149
|
-
chat_id, user_ids, show_history
|
|
150
|
-
)
|
|
154
|
+
return await self.invite_users_to_group(chat_id, user_ids, show_history)
|
|
151
155
|
|
|
152
156
|
async def remove_users_from_group(
|
|
153
157
|
self,
|
|
@@ -191,9 +195,7 @@ class ChatService:
|
|
|
191
195
|
),
|
|
192
196
|
)
|
|
193
197
|
|
|
194
|
-
response = await self.app.invoke(
|
|
195
|
-
Opcode.CHAT_UPDATE, frame.to_payload()
|
|
196
|
-
)
|
|
198
|
+
response = await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload())
|
|
197
199
|
chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
|
|
198
200
|
if chat:
|
|
199
201
|
self._cache_chat(chat)
|
|
@@ -210,9 +212,7 @@ class ChatService:
|
|
|
210
212
|
description=description,
|
|
211
213
|
)
|
|
212
214
|
|
|
213
|
-
response = await self.app.invoke(
|
|
214
|
-
Opcode.CHAT_UPDATE, frame.to_payload()
|
|
215
|
-
)
|
|
215
|
+
response = await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload())
|
|
216
216
|
chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
|
|
217
217
|
if chat:
|
|
218
218
|
self._cache_chat(chat)
|
|
@@ -222,10 +222,12 @@ class ChatService:
|
|
|
222
222
|
if proceed_link is None:
|
|
223
223
|
raise ValueError("Invalid group link")
|
|
224
224
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
return await self._join_chat(proceed_link)
|
|
226
|
+
|
|
227
|
+
async def join_channel(self, link: str) -> Chat:
|
|
228
|
+
proceed_link = self._process_chat_join_link(link)
|
|
229
|
+
|
|
230
|
+
return await self._join_chat(proceed_link or link)
|
|
229
231
|
|
|
230
232
|
async def resolve_group_by_link(self, link: str) -> Chat | None:
|
|
231
233
|
proceed_link = self._process_chat_join_link(link)
|
|
@@ -242,9 +244,7 @@ class ChatService:
|
|
|
242
244
|
|
|
243
245
|
async def rework_invite_link(self, chat_id: int) -> Chat:
|
|
244
246
|
frame = ReworkInviteLinkPayload(chat_id=chat_id)
|
|
245
|
-
response = await self.app.invoke(
|
|
246
|
-
Opcode.CHAT_UPDATE, frame.to_payload()
|
|
247
|
-
)
|
|
247
|
+
response = await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload())
|
|
248
248
|
chat = require_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
|
|
249
249
|
return self._cache_chat(chat)
|
|
250
250
|
|
|
@@ -254,18 +254,12 @@ class ChatService:
|
|
|
254
254
|
for chat_id in chat_ids
|
|
255
255
|
if (chat := self._get_cached_chat(chat_id)) is not None
|
|
256
256
|
}
|
|
257
|
-
missed_chat_ids = [
|
|
258
|
-
chat_id for chat_id in chat_ids if chat_id not in cached
|
|
259
|
-
]
|
|
257
|
+
missed_chat_ids = [chat_id for chat_id in chat_ids if chat_id not in cached]
|
|
260
258
|
|
|
261
259
|
if missed_chat_ids:
|
|
262
260
|
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
|
-
):
|
|
261
|
+
response = await self.app.invoke(Opcode.CHAT_INFO, frame.to_payload())
|
|
262
|
+
for chat in parse_payload_list(response, ChatPayloadKey.CHATS, Chat):
|
|
269
263
|
chat = self._cache_chat(chat)
|
|
270
264
|
cached[chat.id] = chat
|
|
271
265
|
|
|
@@ -292,22 +286,19 @@ class ChatService:
|
|
|
292
286
|
|
|
293
287
|
chats = [
|
|
294
288
|
self._cache_chat(chat)
|
|
295
|
-
for chat in parse_payload_list(
|
|
296
|
-
response, ChatPayloadKey.CHATS, Chat
|
|
297
|
-
)
|
|
289
|
+
for chat in parse_payload_list(response, ChatPayloadKey.CHATS, Chat)
|
|
298
290
|
]
|
|
299
291
|
return chats
|
|
300
292
|
|
|
301
|
-
async def get_join_requests(
|
|
302
|
-
self, chat_id: int, count: int = 100
|
|
303
|
-
) -> list[Member]:
|
|
293
|
+
async def get_join_requests(self, chat_id: int, count: int = 100) -> list[Member]:
|
|
304
294
|
frame = FetchJoinRequests(chat_id=chat_id, count=count)
|
|
305
295
|
|
|
306
|
-
response = await self.app.invoke(
|
|
307
|
-
Opcode.CHAT_MEMBERS, frame.to_payload()
|
|
308
|
-
)
|
|
296
|
+
response = await self.app.invoke(Opcode.CHAT_MEMBERS, frame.to_payload())
|
|
309
297
|
|
|
310
|
-
return
|
|
298
|
+
return bind_api_model(
|
|
299
|
+
self.app,
|
|
300
|
+
parse_payload_list(response, ChatPayloadKey.MEMBERS, Member),
|
|
301
|
+
)
|
|
311
302
|
|
|
312
303
|
async def confirm_join_requests(
|
|
313
304
|
self,
|
|
@@ -322,9 +313,7 @@ class ChatService:
|
|
|
322
313
|
operation=ChatMemberOperation.ADD,
|
|
323
314
|
)
|
|
324
315
|
|
|
325
|
-
response = await self.app.invoke(
|
|
326
|
-
Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload()
|
|
327
|
-
)
|
|
316
|
+
response = await self.app.invoke(Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload())
|
|
328
317
|
|
|
329
318
|
chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
|
|
330
319
|
if chat:
|
|
@@ -356,9 +345,7 @@ class ChatService:
|
|
|
356
345
|
operation=ChatMemberOperation.REMOVE,
|
|
357
346
|
)
|
|
358
347
|
|
|
359
|
-
response = await self.app.invoke(
|
|
360
|
-
Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload()
|
|
361
|
-
)
|
|
348
|
+
response = await self.app.invoke(Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload())
|
|
362
349
|
|
|
363
350
|
chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
|
|
364
351
|
if chat:
|
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
|
@@ -3,10 +3,12 @@ from __future__ import annotations
|
|
|
3
3
|
import time
|
|
4
4
|
from typing import TYPE_CHECKING, TypeAlias
|
|
5
5
|
|
|
6
|
+
from pymax.api.binding import bind_api_model, bind_api_models
|
|
6
7
|
from pymax.api.response import (
|
|
7
8
|
parse_payload_list,
|
|
8
9
|
parse_payload_model,
|
|
9
10
|
payload_item,
|
|
11
|
+
require_payload_item_model,
|
|
10
12
|
require_payload_model,
|
|
11
13
|
)
|
|
12
14
|
from pymax.api.uploads.payloads import (
|
|
@@ -32,7 +34,9 @@ from .payloads import (
|
|
|
32
34
|
AddReactionPayload,
|
|
33
35
|
ChatHistoryPayload,
|
|
34
36
|
DeleteMessagePayload,
|
|
37
|
+
EditMessagePayload,
|
|
35
38
|
GetFilePayload,
|
|
39
|
+
GetMessagesPayload,
|
|
36
40
|
GetReactionsPayload,
|
|
37
41
|
GetVideoPayload,
|
|
38
42
|
PinMessagePayload,
|
|
@@ -69,17 +73,13 @@ class MessageService:
|
|
|
69
73
|
async def _upload_attachments(
|
|
70
74
|
self, attachments: SendAttachments
|
|
71
75
|
) -> list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload]:
|
|
72
|
-
result: list[
|
|
73
|
-
AttachPhotoPayload | VideoAttachPayload | AttachFilePayload
|
|
74
|
-
] = []
|
|
76
|
+
result: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload] = []
|
|
75
77
|
if not attachments:
|
|
76
78
|
return result
|
|
77
79
|
|
|
78
80
|
for attachment in attachments:
|
|
79
81
|
if isinstance(attachment, Photo):
|
|
80
|
-
upload_result = await self.app.api.uploads.upload_photo(
|
|
81
|
-
attachment
|
|
82
|
-
)
|
|
82
|
+
upload_result = await self.app.api.uploads.upload_photo(attachment)
|
|
83
83
|
if not upload_result:
|
|
84
84
|
logger.error("Photo uploading failed")
|
|
85
85
|
raise UploadError("Photo uploading failed")
|
|
@@ -87,9 +87,7 @@ class MessageService:
|
|
|
87
87
|
result.append(upload_result)
|
|
88
88
|
|
|
89
89
|
elif isinstance(attachment, Video):
|
|
90
|
-
upload_result = await self.app.api.uploads.upload_video(
|
|
91
|
-
attachment
|
|
92
|
-
)
|
|
90
|
+
upload_result = await self.app.api.uploads.upload_video(attachment)
|
|
93
91
|
if not upload_result:
|
|
94
92
|
logger.error("Video uploading failed")
|
|
95
93
|
raise UploadError("Video uploading failed")
|
|
@@ -97,9 +95,7 @@ class MessageService:
|
|
|
97
95
|
result.append(upload_result)
|
|
98
96
|
|
|
99
97
|
elif isinstance(attachment, File):
|
|
100
|
-
upload_result = await self.app.api.uploads.upload_file(
|
|
101
|
-
attachment
|
|
102
|
-
)
|
|
98
|
+
upload_result = await self.app.api.uploads.upload_file(attachment)
|
|
103
99
|
if not upload_result:
|
|
104
100
|
logger.error("File uploading failed")
|
|
105
101
|
raise UploadError("File uploading failed")
|
|
@@ -117,9 +113,7 @@ class MessageService:
|
|
|
117
113
|
*,
|
|
118
114
|
notify: bool = True,
|
|
119
115
|
) -> Message | None:
|
|
120
|
-
logger.info(
|
|
121
|
-
"sending message chat_id=%s text_len=%s", chat_id, len(text)
|
|
122
|
-
)
|
|
116
|
+
logger.info("sending message chat_id=%s text_len=%s", chat_id, len(text))
|
|
123
117
|
|
|
124
118
|
clean_text, elements = Formatter.format_markdown(text)
|
|
125
119
|
|
|
@@ -137,10 +131,75 @@ class MessageService:
|
|
|
137
131
|
|
|
138
132
|
response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
|
|
139
133
|
|
|
140
|
-
message =
|
|
134
|
+
message = bind_api_model(
|
|
135
|
+
self.app,
|
|
136
|
+
require_payload_model(response, Message),
|
|
137
|
+
)
|
|
141
138
|
logger.info("message sent chat_id=%s", chat_id)
|
|
142
139
|
return message
|
|
143
140
|
|
|
141
|
+
async def get_messages(
|
|
142
|
+
self,
|
|
143
|
+
chat_id: int,
|
|
144
|
+
message_ids: list[int],
|
|
145
|
+
) -> list[Message]:
|
|
146
|
+
frame = GetMessagesPayload(
|
|
147
|
+
chat_id=chat_id,
|
|
148
|
+
message_ids=message_ids,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
response = await self.app.invoke(Opcode.MSG_GET, frame.to_payload())
|
|
152
|
+
messages = parse_payload_list(response, MessagePayloadKey.MESSAGES, Message)
|
|
153
|
+
for message in messages:
|
|
154
|
+
if message.chat_id is None:
|
|
155
|
+
message.chat_id = chat_id
|
|
156
|
+
|
|
157
|
+
return bind_api_models(self.app, messages)
|
|
158
|
+
|
|
159
|
+
async def get_message(
|
|
160
|
+
self,
|
|
161
|
+
chat_id: int,
|
|
162
|
+
message_id: int,
|
|
163
|
+
) -> Message | None:
|
|
164
|
+
messages = await self.get_messages(chat_id, [message_id])
|
|
165
|
+
return messages[0] if messages else None
|
|
166
|
+
|
|
167
|
+
async def edit_message(
|
|
168
|
+
self,
|
|
169
|
+
chat_id: int,
|
|
170
|
+
message_id: int,
|
|
171
|
+
text: str,
|
|
172
|
+
attachment: SendAttachment | None = None,
|
|
173
|
+
attachments: SendAttachments = None,
|
|
174
|
+
) -> Message:
|
|
175
|
+
if attachment is not None and attachments:
|
|
176
|
+
logger.warning("both attachment and attachments provided; using attachments")
|
|
177
|
+
attachment = None
|
|
178
|
+
|
|
179
|
+
edit_attachments = attachments
|
|
180
|
+
if attachment is not None:
|
|
181
|
+
edit_attachments = [attachment]
|
|
182
|
+
|
|
183
|
+
clean_text, elements = Formatter.format_markdown(text)
|
|
184
|
+
frame = EditMessagePayload(
|
|
185
|
+
chat_id=chat_id,
|
|
186
|
+
message_id=message_id,
|
|
187
|
+
text=clean_text,
|
|
188
|
+
elements=elements,
|
|
189
|
+
attachments=await self._upload_attachments(edit_attachments),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
|
|
193
|
+
message = require_payload_item_model(
|
|
194
|
+
response,
|
|
195
|
+
MessagePayloadKey.MESSAGE,
|
|
196
|
+
Message,
|
|
197
|
+
)
|
|
198
|
+
if message.chat_id is None:
|
|
199
|
+
message.chat_id = chat_id
|
|
200
|
+
|
|
201
|
+
return bind_api_model(self.app, message)
|
|
202
|
+
|
|
144
203
|
async def fetch_history(
|
|
145
204
|
self,
|
|
146
205
|
chat_id: int,
|
|
@@ -171,10 +230,11 @@ class MessageService:
|
|
|
171
230
|
Opcode.CHAT_HISTORY,
|
|
172
231
|
payload=frame.to_payload(),
|
|
173
232
|
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
233
|
+
messages = bind_api_models(
|
|
234
|
+
self.app,
|
|
235
|
+
parse_payload_list(response, MessagePayloadKey.MESSAGES, Message),
|
|
177
236
|
)
|
|
237
|
+
return messages or None
|
|
178
238
|
|
|
179
239
|
async def delete_message(
|
|
180
240
|
self,
|
|
@@ -195,9 +255,7 @@ class MessageService:
|
|
|
195
255
|
)
|
|
196
256
|
|
|
197
257
|
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
|
-
)
|
|
258
|
+
logger.info("messages deleted chat_id=%s count=%s", chat_id, len(message_ids))
|
|
201
259
|
return True
|
|
202
260
|
|
|
203
261
|
async def pin_message(
|
|
@@ -219,9 +277,7 @@ class MessageService:
|
|
|
219
277
|
)
|
|
220
278
|
|
|
221
279
|
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
|
-
)
|
|
280
|
+
logger.info("message pinned chat_id=%s message_id=%s", chat_id, message_id)
|
|
225
281
|
return True
|
|
226
282
|
|
|
227
283
|
async def get_video_by_id(
|
|
@@ -263,9 +319,7 @@ class MessageService:
|
|
|
263
319
|
file_id=file_id,
|
|
264
320
|
)
|
|
265
321
|
|
|
266
|
-
response = await self.app.invoke(
|
|
267
|
-
Opcode.FILE_DOWNLOAD, frame.to_payload()
|
|
268
|
-
)
|
|
322
|
+
response = await self.app.invoke(Opcode.FILE_DOWNLOAD, frame.to_payload())
|
|
269
323
|
return parse_payload_model(response, FileRequest)
|
|
270
324
|
|
|
271
325
|
async def add_reaction(
|
|
@@ -286,9 +340,7 @@ class MessageService:
|
|
|
286
340
|
reaction=ReactionInfoPayload(id=reaction),
|
|
287
341
|
)
|
|
288
342
|
|
|
289
|
-
response = await self.app.invoke(
|
|
290
|
-
Opcode.MSG_REACTION, frame.to_payload()
|
|
291
|
-
)
|
|
343
|
+
response = await self.app.invoke(Opcode.MSG_REACTION, frame.to_payload())
|
|
292
344
|
reaction_info = payload_item(response, MessagePayloadKey.REACTION_INFO)
|
|
293
345
|
if reaction_info:
|
|
294
346
|
return ReactionInfo.model_validate(reaction_info)
|
|
@@ -345,7 +397,7 @@ class MessageService:
|
|
|
345
397
|
|
|
346
398
|
return None
|
|
347
399
|
|
|
348
|
-
async def read_message(self, message_id: int, chat_id: int) -> ReadState:
|
|
400
|
+
async def read_message(self, message_id: int | str, chat_id: int) -> ReadState:
|
|
349
401
|
logger.info(
|
|
350
402
|
"marking message as read chat_id=%s message_id=%s",
|
|
351
403
|
chat_id,
|
|
@@ -354,7 +406,7 @@ class MessageService:
|
|
|
354
406
|
frame = ReadMessagesPayload(
|
|
355
407
|
type=ReadAction.READ_MESSAGE,
|
|
356
408
|
chat_id=chat_id,
|
|
357
|
-
message_id=
|
|
409
|
+
message_id=message_id,
|
|
358
410
|
mark=int(time.time() * 1000),
|
|
359
411
|
)
|
|
360
412
|
|
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
|
|