maxapi-python 2.4.0__py3-none-any.whl → 2.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/METADATA +22 -12
- maxapi_python-2.5.0.dist-info/RECORD +195 -0
- {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +8 -2
- pymax/_data/apk_fingerprints.json +374 -17
- pymax/_data/rootca_ssl_rsa2022.crt +33 -0
- pymax/api/auth/service.py +15 -2
- pymax/api/auth/types.py +3 -0
- pymax/api/bots/payloads.py +8 -0
- pymax/api/bots/service.py +13 -1
- pymax/api/chats/enums.py +1 -0
- pymax/api/chats/payloads.py +21 -1
- pymax/api/chats/service.py +41 -0
- pymax/api/facade.py +2 -0
- pymax/api/messages/__init__.py +1 -1
- pymax/api/messages/enums.py +1 -0
- pymax/api/messages/payloads.py +56 -4
- pymax/api/messages/service.py +405 -27
- pymax/api/response.py +1 -1
- pymax/api/stories/__init__.py +2 -0
- pymax/api/stories/enums.py +19 -0
- pymax/api/stories/payloads.py +64 -0
- pymax/api/stories/service.py +198 -0
- pymax/api/uploads/payloads.py +42 -1
- pymax/api/uploads/service.py +171 -69
- pymax/app.py +39 -8
- pymax/auth/__init__.py +2 -0
- pymax/auth/exceptions.py +9 -0
- pymax/auth/providers.py +6 -2
- pymax/auth/qr.py +11 -1
- pymax/auth/sms.py +11 -1
- pymax/base.py +146 -27
- pymax/client.py +39 -11
- pymax/client_web.py +15 -12
- pymax/config.py +101 -53
- pymax/di/__init__.py +2 -0
- pymax/di/context.py +18 -0
- pymax/di/resolver.py +38 -0
- pymax/dispatch/__init__.py +7 -0
- pymax/dispatch/dispatcher.py +143 -40
- pymax/dispatch/enums.py +1 -0
- pymax/dispatch/mapping.py +23 -3
- pymax/dispatch/middleware.py +32 -0
- pymax/dispatch/resolvers.py +4 -0
- pymax/dispatch/router.py +86 -34
- pymax/files/base.py +58 -1
- pymax/files/video.py +32 -40
- pymax/files/voice.py +18 -4
- pymax/filters/__init__.py +15 -0
- pymax/filters/base.py +218 -0
- pymax/filters/containers/__init__.py +2 -0
- pymax/filters/containers/chat.py +240 -0
- pymax/filters/containers/message.py +147 -0
- pymax/filters/filters.py +211 -0
- pymax/filters/magic.py +13 -0
- pymax/filters/presets.py +88 -0
- pymax/filters/refs.py +56 -0
- pymax/fingerprint/fingerprint.py +5 -22
- pymax/fingerprint/models.py +1 -0
- pymax/formatting/markdown.py +55 -1
- pymax/fsm/__init__.py +3 -0
- pymax/fsm/context.py +30 -0
- pymax/fsm/protocol.py +14 -0
- pymax/fsm/state.py +70 -0
- pymax/fsm/storage.py +151 -0
- pymax/infra/base.py +2 -0
- pymax/infra/bots.py +20 -0
- pymax/infra/chat.py +37 -5
- pymax/infra/message.py +204 -9
- pymax/infra/protocol.py +5 -1
- pymax/infra/stories.py +205 -0
- pymax/integrations/__init__.py +0 -0
- pymax/integrations/dishka.py +109 -0
- pymax/protocol/enums.py +2 -0
- pymax/protocol/tcp/payload.py +0 -2
- pymax/session/__init__.py +1 -1
- pymax/session/models.py +30 -0
- pymax/session/protocol.py +33 -7
- pymax/session/store.py +73 -5
- pymax/transport/tcp.py +11 -3
- pymax/types/domain/__init__.py +23 -1
- pymax/types/domain/attachments/__init__.py +1 -1
- pymax/types/domain/attachments/call.py +7 -1
- pymax/types/domain/attachments/contact.py +3 -3
- pymax/types/domain/attachments/control.py +3 -0
- pymax/types/domain/attachments/enums.py +16 -0
- pymax/types/domain/attachments/keyboards/__init__.py +1 -1
- pymax/types/domain/attachments/keyboards/enums.py +12 -0
- pymax/types/domain/attachments/keyboards/inline.py +74 -3
- pymax/types/domain/attachments/video.py +3 -3
- pymax/types/domain/callback.py +19 -0
- pymax/types/domain/comment.py +24 -0
- pymax/types/domain/enums.py +7 -0
- pymax/types/domain/message.py +158 -9
- pymax/types/domain/story.py +126 -0
- pymax/types/events/reaction.py +28 -4
- pymax/versions/__init__.py +2 -0
- pymax/versions/catalog.py +104 -0
- pymax/versions/exceptions.py +10 -0
- maxapi_python-2.4.0.dist-info/RECORD +0 -161
- {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/licenses/LICENSE +0 -0
pymax/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,13 +18,16 @@ 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
|
|
24
|
+
from pymax.exceptions import ApiError, UploadError
|
|
21
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 (
|
|
30
|
+
CommentsInfoUpdate,
|
|
26
31
|
FileRequest,
|
|
27
32
|
Message,
|
|
28
33
|
Poll,
|
|
@@ -34,13 +39,21 @@ from pymax.types.domain import (
|
|
|
34
39
|
|
|
35
40
|
from .enums import ItemType, MessagePayloadKey, ReadAction
|
|
36
41
|
from .payloads import (
|
|
42
|
+
AddCommentReactionPayload,
|
|
37
43
|
AddReactionPayload,
|
|
38
44
|
ChatHistoryPayload,
|
|
45
|
+
CommentsHistoryPayload,
|
|
46
|
+
DelayedAttributes,
|
|
47
|
+
DeleteCommentPayload,
|
|
39
48
|
DeleteMessagePayload,
|
|
49
|
+
DeleteUserCommentsPayload,
|
|
50
|
+
EditCommentPayload,
|
|
40
51
|
EditMessagePayload,
|
|
41
52
|
ForwardLink,
|
|
42
53
|
ForwardMessagePayload,
|
|
43
54
|
ForwardMessagePayloadMessage,
|
|
55
|
+
GetCommentsInfoPayload,
|
|
56
|
+
GetCommentsPayload,
|
|
44
57
|
GetFilePayload,
|
|
45
58
|
GetMessagesPayload,
|
|
46
59
|
GetReactionsPayload,
|
|
@@ -48,10 +61,13 @@ from .payloads import (
|
|
|
48
61
|
PinMessagePayload,
|
|
49
62
|
ReactionInfoPayload,
|
|
50
63
|
ReadMessagesPayload,
|
|
64
|
+
RemoveCommentReactionPayload,
|
|
51
65
|
RemoveReactionPayload,
|
|
52
66
|
ReplyLink,
|
|
67
|
+
SendCommentPayload,
|
|
53
68
|
SendMessagePayload,
|
|
54
69
|
SendMessagePayloadMessage,
|
|
70
|
+
SubscribeCommentsPayload,
|
|
55
71
|
VotePollPayload,
|
|
56
72
|
)
|
|
57
73
|
|
|
@@ -60,6 +76,9 @@ if TYPE_CHECKING:
|
|
|
60
76
|
|
|
61
77
|
SendAttachment: TypeAlias = Photo | File | Video | Poll | Voice | VideoNote
|
|
62
78
|
SendAttachments: TypeAlias = Sequence[SendAttachment] | None
|
|
79
|
+
DateTimeUnion: TypeAlias = datetime | timedelta | int
|
|
80
|
+
|
|
81
|
+
T = TypeVar("T")
|
|
63
82
|
|
|
64
83
|
logger = get_logger(__name__)
|
|
65
84
|
|
|
@@ -77,10 +96,48 @@ class MessageService:
|
|
|
77
96
|
logger.debug("generated message cid=%s", e)
|
|
78
97
|
return e
|
|
79
98
|
|
|
99
|
+
async def _prepare_message(
|
|
100
|
+
self,
|
|
101
|
+
text: str | None,
|
|
102
|
+
reply_to: int | None,
|
|
103
|
+
attachments: SendAttachments,
|
|
104
|
+
*,
|
|
105
|
+
delayed_attributes: DelayedAttributes | None = None,
|
|
106
|
+
) -> tuple[
|
|
107
|
+
SendMessagePayloadMessage,
|
|
108
|
+
list[
|
|
109
|
+
AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | VoiceAttachPayload | Poll
|
|
110
|
+
],
|
|
111
|
+
]:
|
|
112
|
+
if not text and not attachments:
|
|
113
|
+
raise ValueError("Either text or attachments must be provided")
|
|
114
|
+
|
|
115
|
+
if text:
|
|
116
|
+
clean_text, elements = Formatter.format_markdown(text)
|
|
117
|
+
else:
|
|
118
|
+
clean_text, elements = None, []
|
|
119
|
+
|
|
120
|
+
attaches = await self._upload_attachments(attachments)
|
|
121
|
+
|
|
122
|
+
message = SendMessagePayloadMessage(
|
|
123
|
+
text=clean_text,
|
|
124
|
+
cid=self._next_cid(),
|
|
125
|
+
elements=elements,
|
|
126
|
+
attaches=attaches,
|
|
127
|
+
link=ReplyLink(message_id=reply_to) if reply_to else None,
|
|
128
|
+
delayed_attributes=delayed_attributes,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return message, attaches
|
|
132
|
+
|
|
80
133
|
async def _upload_attachments(
|
|
81
134
|
self, attachments: SendAttachments
|
|
82
|
-
) -> list[
|
|
83
|
-
|
|
135
|
+
) -> list[
|
|
136
|
+
AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | VoiceAttachPayload | Poll
|
|
137
|
+
]: # TODO: TypeAlias
|
|
138
|
+
result: list[
|
|
139
|
+
AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | VoiceAttachPayload | Poll
|
|
140
|
+
] = []
|
|
84
141
|
if not attachments:
|
|
85
142
|
return result
|
|
86
143
|
|
|
@@ -101,7 +158,7 @@ class MessageService:
|
|
|
101
158
|
|
|
102
159
|
result.append(upload_result)
|
|
103
160
|
|
|
104
|
-
elif isinstance(attachment, Video):
|
|
161
|
+
elif isinstance(attachment, (Video, VideoNote)):
|
|
105
162
|
upload_result = await self.app.api.uploads.upload_video(attachment)
|
|
106
163
|
if not upload_result:
|
|
107
164
|
logger.error("Video uploading failed")
|
|
@@ -122,6 +179,62 @@ class MessageService:
|
|
|
122
179
|
|
|
123
180
|
return result
|
|
124
181
|
|
|
182
|
+
def _convert_time(self, time: DateTimeUnion) -> int:
|
|
183
|
+
if isinstance(time, datetime):
|
|
184
|
+
return int(time.timestamp() * 1000)
|
|
185
|
+
|
|
186
|
+
if isinstance(time, timedelta):
|
|
187
|
+
return int((datetime.now() + time).timestamp() * 1000)
|
|
188
|
+
|
|
189
|
+
return time * 1000
|
|
190
|
+
|
|
191
|
+
async def _wait_for_upload_signal(
|
|
192
|
+
self,
|
|
193
|
+
waiters: dict[int, asyncio.Future[T]],
|
|
194
|
+
video_id: int,
|
|
195
|
+
) -> None:
|
|
196
|
+
loop = asyncio.get_running_loop()
|
|
197
|
+
future: asyncio.Future[T] = loop.create_future()
|
|
198
|
+
|
|
199
|
+
waiters[video_id] = future
|
|
200
|
+
try:
|
|
201
|
+
await asyncio.wait_for(future, timeout=60)
|
|
202
|
+
except TimeoutError:
|
|
203
|
+
logger.warning(
|
|
204
|
+
"Timed out waiting for video processing notification video_id=%s",
|
|
205
|
+
video_id,
|
|
206
|
+
)
|
|
207
|
+
raise UploadError(f"Timed out waiting for video processing video_id={video_id}")
|
|
208
|
+
finally:
|
|
209
|
+
waiters.pop(video_id, None)
|
|
210
|
+
|
|
211
|
+
async def _process_attachment_error(
|
|
212
|
+
self,
|
|
213
|
+
attaches: list[
|
|
214
|
+
AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | VoiceAttachPayload | Poll
|
|
215
|
+
],
|
|
216
|
+
):
|
|
217
|
+
attachment: VoiceAttachPayload | VideoNoteAttachPayload | None = None
|
|
218
|
+
for attach in attaches:
|
|
219
|
+
if isinstance(attach, (VoiceAttachPayload, VideoNoteAttachPayload)):
|
|
220
|
+
attachment = attach
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
if attachment is None:
|
|
224
|
+
raise ValueError(
|
|
225
|
+
"Unexpected internal state: Non voice/video_note attach is not ready. "
|
|
226
|
+
+ "Please, report that issue"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if isinstance(attachment, VoiceAttachPayload):
|
|
230
|
+
await self._wait_for_upload_signal(
|
|
231
|
+
self.app.api.uploads.voice_upload_waiters, attachment.video_id
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
await self._wait_for_upload_signal(
|
|
235
|
+
self.app.api.uploads.video_upload_waiters, attachment.video_id
|
|
236
|
+
)
|
|
237
|
+
|
|
125
238
|
async def send_message(
|
|
126
239
|
self,
|
|
127
240
|
chat_id: int,
|
|
@@ -130,31 +243,36 @@ class MessageService:
|
|
|
130
243
|
attachments: SendAttachments = None,
|
|
131
244
|
*,
|
|
132
245
|
notify: bool = True,
|
|
246
|
+
send_at: DateTimeUnion | None = None,
|
|
133
247
|
) -> Message:
|
|
134
248
|
logger.info("sending message chat_id=%s text_len=%s", chat_id, len(text) if text else 0)
|
|
135
249
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
250
|
+
message, attaches = await self._prepare_message(
|
|
251
|
+
text,
|
|
252
|
+
reply_to,
|
|
253
|
+
attachments,
|
|
254
|
+
delayed_attributes=DelayedAttributes(
|
|
255
|
+
time_to_fire=self._convert_time(send_at),
|
|
256
|
+
notify_sender=notify,
|
|
257
|
+
)
|
|
258
|
+
if send_at
|
|
259
|
+
else None,
|
|
260
|
+
)
|
|
144
261
|
|
|
145
262
|
frame = SendMessagePayload(
|
|
146
263
|
chat_id=chat_id,
|
|
147
|
-
message=
|
|
148
|
-
text=clean_text,
|
|
149
|
-
cid=self._next_cid(),
|
|
150
|
-
elements=elements,
|
|
151
|
-
attaches=await self._upload_attachments(attachments),
|
|
152
|
-
link=ReplyLink(message_id=reply_to) if reply_to else None,
|
|
153
|
-
),
|
|
264
|
+
message=message,
|
|
154
265
|
notify=notify,
|
|
155
266
|
)
|
|
156
267
|
|
|
157
|
-
|
|
268
|
+
try:
|
|
269
|
+
response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
|
|
270
|
+
except ApiError as e:
|
|
271
|
+
if e.error == "attachment.not.ready":
|
|
272
|
+
await self._process_attachment_error(attaches)
|
|
273
|
+
response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
|
|
274
|
+
else:
|
|
275
|
+
raise
|
|
158
276
|
|
|
159
277
|
message = bind_api_model(
|
|
160
278
|
self.app,
|
|
@@ -241,15 +359,25 @@ class MessageService:
|
|
|
241
359
|
else:
|
|
242
360
|
clean_text, elements = None, []
|
|
243
361
|
|
|
362
|
+
attaches = await self._upload_attachments(attachments)
|
|
363
|
+
|
|
244
364
|
frame = EditMessagePayload(
|
|
245
365
|
chat_id=chat_id,
|
|
246
366
|
message_id=message_id,
|
|
247
367
|
text=clean_text,
|
|
248
368
|
elements=elements,
|
|
249
|
-
attachments=
|
|
369
|
+
attachments=attaches,
|
|
250
370
|
)
|
|
371
|
+
try:
|
|
372
|
+
response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
|
|
373
|
+
except ApiError as e:
|
|
374
|
+
if e.error == "attachment.not.ready":
|
|
375
|
+
await self._process_attachment_error(attaches)
|
|
376
|
+
|
|
377
|
+
response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
|
|
378
|
+
else:
|
|
379
|
+
raise
|
|
251
380
|
|
|
252
|
-
response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
|
|
253
381
|
message = require_payload_item_model(
|
|
254
382
|
response,
|
|
255
383
|
MessagePayloadKey.MESSAGE,
|
|
@@ -296,6 +424,33 @@ class MessageService:
|
|
|
296
424
|
)
|
|
297
425
|
return messages
|
|
298
426
|
|
|
427
|
+
async def fetch_comments(
|
|
428
|
+
self,
|
|
429
|
+
chat_id: int,
|
|
430
|
+
post_id: int,
|
|
431
|
+
from_: int | None = None,
|
|
432
|
+
backward: int = 30,
|
|
433
|
+
get_messages: bool = True,
|
|
434
|
+
forward: int = 0,
|
|
435
|
+
) -> list[Message]:
|
|
436
|
+
frame = CommentsHistoryPayload(
|
|
437
|
+
chat_id=chat_id,
|
|
438
|
+
forward=forward,
|
|
439
|
+
backward=backward,
|
|
440
|
+
post_id=post_id,
|
|
441
|
+
from_=-1 if from_ is None else from_,
|
|
442
|
+
get_messages=get_messages,
|
|
443
|
+
)
|
|
444
|
+
response = await self.app.invoke(
|
|
445
|
+
Opcode.CHAT_HISTORY,
|
|
446
|
+
payload=frame.to_payload(),
|
|
447
|
+
)
|
|
448
|
+
messages = bind_api_models(
|
|
449
|
+
self.app,
|
|
450
|
+
parse_payload_list(response, MessagePayloadKey.MESSAGES, Message),
|
|
451
|
+
)
|
|
452
|
+
return messages
|
|
453
|
+
|
|
299
454
|
async def delete_message(
|
|
300
455
|
self,
|
|
301
456
|
chat_id: int,
|
|
@@ -385,7 +540,7 @@ class MessageService:
|
|
|
385
540
|
async def add_reaction(
|
|
386
541
|
self,
|
|
387
542
|
chat_id: int,
|
|
388
|
-
message_id:
|
|
543
|
+
message_id: int,
|
|
389
544
|
reaction: str,
|
|
390
545
|
) -> ReactionInfo | None:
|
|
391
546
|
logger.info(
|
|
@@ -410,7 +565,7 @@ class MessageService:
|
|
|
410
565
|
async def get_reactions(
|
|
411
566
|
self,
|
|
412
567
|
chat_id: int,
|
|
413
|
-
message_ids: list[
|
|
568
|
+
message_ids: list[int],
|
|
414
569
|
) -> dict[str, ReactionInfo] | None:
|
|
415
570
|
logger.info(
|
|
416
571
|
"getting reactions chat_id=%s message_ids=%s",
|
|
@@ -438,7 +593,7 @@ class MessageService:
|
|
|
438
593
|
async def remove_reaction(
|
|
439
594
|
self,
|
|
440
595
|
chat_id: int,
|
|
441
|
-
message_id:
|
|
596
|
+
message_id: int,
|
|
442
597
|
) -> ReactionInfo | None:
|
|
443
598
|
logger.info(
|
|
444
599
|
"removing reaction chat_id=%s message_id=%s",
|
|
@@ -457,7 +612,7 @@ class MessageService:
|
|
|
457
612
|
|
|
458
613
|
return None
|
|
459
614
|
|
|
460
|
-
async def read_message(self, message_id: int
|
|
615
|
+
async def read_message(self, message_id: int, chat_id: int) -> ReadState:
|
|
461
616
|
logger.info(
|
|
462
617
|
"marking message as read chat_id=%s message_id=%s",
|
|
463
618
|
chat_id,
|
|
@@ -486,3 +641,226 @@ class MessageService:
|
|
|
486
641
|
response = await self.app.invoke(Opcode.SEND_VOTE, frame.to_payload())
|
|
487
642
|
|
|
488
643
|
return require_payload_item_model(response, "state", PollState)
|
|
644
|
+
|
|
645
|
+
async def send_comment(
|
|
646
|
+
self,
|
|
647
|
+
chat_id: int,
|
|
648
|
+
post_id: int,
|
|
649
|
+
text: str | None = None,
|
|
650
|
+
reply_to: int | None = None,
|
|
651
|
+
attachments: SendAttachments = None,
|
|
652
|
+
*,
|
|
653
|
+
notify: bool = True,
|
|
654
|
+
) -> Message:
|
|
655
|
+
message, attaches = await self._prepare_message(text, reply_to, attachments)
|
|
656
|
+
|
|
657
|
+
frame = SendCommentPayload(
|
|
658
|
+
chat_id=chat_id,
|
|
659
|
+
post_id=post_id,
|
|
660
|
+
notify=notify,
|
|
661
|
+
message=message,
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
try:
|
|
665
|
+
response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
|
|
666
|
+
except ApiError as e:
|
|
667
|
+
if e.error == "attachment.not.ready":
|
|
668
|
+
await self._process_attachment_error(attaches)
|
|
669
|
+
response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
|
|
670
|
+
else:
|
|
671
|
+
raise
|
|
672
|
+
|
|
673
|
+
return bind_api_model(
|
|
674
|
+
self.app,
|
|
675
|
+
require_payload_model(response, Message),
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
async def get_comments(
|
|
679
|
+
self,
|
|
680
|
+
chat_id: int,
|
|
681
|
+
post_id: int,
|
|
682
|
+
message_ids: list[int],
|
|
683
|
+
) -> list[Message]:
|
|
684
|
+
frame = GetCommentsPayload(
|
|
685
|
+
chat_id=chat_id,
|
|
686
|
+
post_id=post_id,
|
|
687
|
+
message_ids=message_ids,
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
response = await self.app.invoke(Opcode.MSG_GET, frame.to_payload())
|
|
691
|
+
messages = parse_payload_list(response, MessagePayloadKey.MESSAGES, Message)
|
|
692
|
+
for message in messages:
|
|
693
|
+
if message.chat_id is None:
|
|
694
|
+
message.chat_id = chat_id
|
|
695
|
+
|
|
696
|
+
return bind_api_models(self.app, messages)
|
|
697
|
+
|
|
698
|
+
async def get_comment(
|
|
699
|
+
self,
|
|
700
|
+
chat_id: int,
|
|
701
|
+
post_id: int,
|
|
702
|
+
message_id: int,
|
|
703
|
+
) -> Message | None:
|
|
704
|
+
messages = await self.get_comments(chat_id, post_id, [message_id])
|
|
705
|
+
return messages[0] if messages else None
|
|
706
|
+
|
|
707
|
+
async def edit_comment(
|
|
708
|
+
self,
|
|
709
|
+
chat_id: int,
|
|
710
|
+
post_id: int,
|
|
711
|
+
message_id: int,
|
|
712
|
+
text: str | None = None,
|
|
713
|
+
attachments: SendAttachments = None,
|
|
714
|
+
) -> Message:
|
|
715
|
+
if not text and not attachments:
|
|
716
|
+
logger.error("edit_comment failed: no text or attachments provided")
|
|
717
|
+
raise ValueError("Either text or attachments must be provided")
|
|
718
|
+
|
|
719
|
+
if text:
|
|
720
|
+
clean_text, elements = Formatter.format_markdown(text)
|
|
721
|
+
else:
|
|
722
|
+
clean_text, elements = None, []
|
|
723
|
+
|
|
724
|
+
attaches = await self._upload_attachments(attachments)
|
|
725
|
+
|
|
726
|
+
frame = EditCommentPayload(
|
|
727
|
+
chat_id=chat_id,
|
|
728
|
+
post_id=post_id,
|
|
729
|
+
message_id=message_id,
|
|
730
|
+
text=clean_text,
|
|
731
|
+
elements=elements,
|
|
732
|
+
attachments=attaches,
|
|
733
|
+
)
|
|
734
|
+
try:
|
|
735
|
+
response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
|
|
736
|
+
except ApiError as e:
|
|
737
|
+
if e.error == "attachment.not.ready":
|
|
738
|
+
await self._process_attachment_error(attaches)
|
|
739
|
+
|
|
740
|
+
response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
|
|
741
|
+
else:
|
|
742
|
+
raise
|
|
743
|
+
|
|
744
|
+
message = require_payload_item_model(
|
|
745
|
+
response,
|
|
746
|
+
MessagePayloadKey.MESSAGE,
|
|
747
|
+
Message,
|
|
748
|
+
)
|
|
749
|
+
if message.chat_id is None:
|
|
750
|
+
message.chat_id = chat_id
|
|
751
|
+
|
|
752
|
+
return bind_api_model(self.app, message)
|
|
753
|
+
|
|
754
|
+
async def delete_comment(
|
|
755
|
+
self,
|
|
756
|
+
chat_id: int,
|
|
757
|
+
post_id: int,
|
|
758
|
+
message_ids: list[int],
|
|
759
|
+
for_me: bool = False,
|
|
760
|
+
) -> bool:
|
|
761
|
+
logger.info(
|
|
762
|
+
"deleting messages chat_id=%s ids=%s for_me=%s",
|
|
763
|
+
chat_id,
|
|
764
|
+
message_ids,
|
|
765
|
+
for_me,
|
|
766
|
+
)
|
|
767
|
+
frame = DeleteCommentPayload(
|
|
768
|
+
chat_id=chat_id,
|
|
769
|
+
post_id=post_id,
|
|
770
|
+
message_ids=message_ids,
|
|
771
|
+
for_me=for_me,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
await self.app.invoke(Opcode.MSG_DELETE, frame.to_payload())
|
|
775
|
+
logger.info("messages deleted chat_id=%s count=%s", chat_id, len(message_ids))
|
|
776
|
+
return True
|
|
777
|
+
|
|
778
|
+
async def add_comment_reaction(
|
|
779
|
+
self,
|
|
780
|
+
chat_id: int,
|
|
781
|
+
post_id: int,
|
|
782
|
+
message_id: int,
|
|
783
|
+
reaction: str,
|
|
784
|
+
) -> ReactionInfo | None:
|
|
785
|
+
logger.info(
|
|
786
|
+
"adding reaction chat_id=%s message_id=%s reaction=%s",
|
|
787
|
+
chat_id,
|
|
788
|
+
message_id,
|
|
789
|
+
reaction,
|
|
790
|
+
)
|
|
791
|
+
frame = AddCommentReactionPayload(
|
|
792
|
+
chat_id=chat_id,
|
|
793
|
+
post_id=post_id,
|
|
794
|
+
message_id=message_id,
|
|
795
|
+
reaction=ReactionInfoPayload(id=reaction),
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
response = await self.app.invoke(Opcode.MSG_REACTION, frame.to_payload())
|
|
799
|
+
reaction_info = payload_item(response, MessagePayloadKey.REACTION_INFO)
|
|
800
|
+
if reaction_info:
|
|
801
|
+
return ReactionInfo.model_validate(reaction_info)
|
|
802
|
+
|
|
803
|
+
return None
|
|
804
|
+
|
|
805
|
+
async def remove_comment_reaction(
|
|
806
|
+
self,
|
|
807
|
+
chat_id: int,
|
|
808
|
+
post_id: int,
|
|
809
|
+
message_id: int,
|
|
810
|
+
) -> ReactionInfo | None:
|
|
811
|
+
logger.info(
|
|
812
|
+
"removing reaction chat_id=%s message_id=%s",
|
|
813
|
+
chat_id,
|
|
814
|
+
message_id,
|
|
815
|
+
)
|
|
816
|
+
frame = RemoveCommentReactionPayload(
|
|
817
|
+
chat_id=chat_id, post_id=post_id, message_id=message_id
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
response = await self.app.invoke(
|
|
821
|
+
Opcode.MSG_CANCEL_REACTION,
|
|
822
|
+
frame.to_payload(),
|
|
823
|
+
)
|
|
824
|
+
reaction_info = payload_item(response, MessagePayloadKey.REACTION_INFO)
|
|
825
|
+
if reaction_info:
|
|
826
|
+
return ReactionInfo.model_validate(reaction_info)
|
|
827
|
+
|
|
828
|
+
return None
|
|
829
|
+
|
|
830
|
+
async def subscribe_comments(
|
|
831
|
+
self,
|
|
832
|
+
chat_id: int,
|
|
833
|
+
post_id: int,
|
|
834
|
+
subscribe: bool = True,
|
|
835
|
+
) -> None:
|
|
836
|
+
frame = SubscribeCommentsPayload(chat_id=chat_id, post_id=post_id, subscribe=subscribe)
|
|
837
|
+
await self.app.invoke(Opcode.CHAT_SUBSCRIBE, frame.to_payload())
|
|
838
|
+
|
|
839
|
+
async def get_comments_info(
|
|
840
|
+
self,
|
|
841
|
+
chat_id: int,
|
|
842
|
+
post_ids: list[int],
|
|
843
|
+
) -> list[CommentsInfoUpdate]:
|
|
844
|
+
frame = GetCommentsInfoPayload(chat_id=chat_id, post_ids=post_ids)
|
|
845
|
+
response = await self.app.invoke(Opcode.MSG_GET_COMMENTS_INFO, frame.to_payload())
|
|
846
|
+
return parse_payload_list(
|
|
847
|
+
response,
|
|
848
|
+
MessagePayloadKey.COMMENTS_INFO_UPDATES,
|
|
849
|
+
CommentsInfoUpdate,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
async def delete_user_comments(
|
|
853
|
+
self,
|
|
854
|
+
chat_id: int,
|
|
855
|
+
post_id: int,
|
|
856
|
+
user_id: int,
|
|
857
|
+
message_id: int,
|
|
858
|
+
) -> bool:
|
|
859
|
+
frame = DeleteUserCommentsPayload(
|
|
860
|
+
chat_id=chat_id,
|
|
861
|
+
post_id=post_id,
|
|
862
|
+
user_id=user_id,
|
|
863
|
+
message_id=message_id,
|
|
864
|
+
)
|
|
865
|
+
await self.app.invoke(Opcode.MSG_DELETE_USER_COMMENTS, frame.to_payload())
|
|
866
|
+
return True
|
pymax/api/response.py
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StoryStatsFilter(int, Enum):
|
|
5
|
+
"""Фильтр подробной статистики: просмотры (``0``) или реакции (``1``)."""
|
|
6
|
+
|
|
7
|
+
VIEWS = 0
|
|
8
|
+
REACTIONS = 1
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StoryPayloadKey(str, Enum):
|
|
12
|
+
STORIES = "stories"
|
|
13
|
+
PEER_STORIES = "peerStories"
|
|
14
|
+
STORY = "story"
|
|
15
|
+
STORIES_PREVIEWS = "storiesPreviews"
|
|
16
|
+
CURSOR = "cursor"
|
|
17
|
+
STORY_DETAILED_STATS = "storyDetailedStats"
|
|
18
|
+
MARKER = "marker"
|
|
19
|
+
SUCCESS = "success"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from pydantic import Field
|
|
2
|
+
|
|
3
|
+
from pymax.api.models import CamelModel
|
|
4
|
+
from pymax.api.uploads.payloads import AttachPhotoPayload, StoryVideoAttachPayload
|
|
5
|
+
from pymax.types.domain.story import StoryLayer, StoryOwner
|
|
6
|
+
|
|
7
|
+
from .enums import StoryStatsFilter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SendStoryPayloadStory(CamelModel):
|
|
11
|
+
cid: int
|
|
12
|
+
media: AttachPhotoPayload | StoryVideoAttachPayload
|
|
13
|
+
settings: int = 1
|
|
14
|
+
expiration: int = 172_800_000
|
|
15
|
+
layers: list[StoryLayer] = Field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SendStoryPayload(CamelModel):
|
|
19
|
+
stories: list[SendStoryPayloadStory]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GetStoriesPayload(CamelModel):
|
|
23
|
+
owners: list[StoryOwner]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DeleteStoryPayload(CamelModel):
|
|
27
|
+
story_ids: list[int]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StoriesListPayload(CamelModel):
|
|
31
|
+
cursor: str = "0"
|
|
32
|
+
count: int = 15
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GetStoriesByIdPayload(CamelModel):
|
|
36
|
+
owner: StoryOwner
|
|
37
|
+
story_ids: list[int]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GetStoryDetailedStatsPayload(CamelModel):
|
|
41
|
+
story_id: int
|
|
42
|
+
marker: int = 0
|
|
43
|
+
filter: StoryStatsFilter
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MarkStoryPayload(CamelModel):
|
|
47
|
+
owner: StoryOwner
|
|
48
|
+
story_id: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class EditStoryPayload(CamelModel):
|
|
52
|
+
story_id: int
|
|
53
|
+
settings: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class StoryReactionPayload(CamelModel):
|
|
57
|
+
reaction_type: int = 0
|
|
58
|
+
id: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ReactStoryPayload(CamelModel):
|
|
62
|
+
owner: StoryOwner
|
|
63
|
+
story_id: int
|
|
64
|
+
reaction: StoryReactionPayload | None = None
|