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.
Files changed (101) hide show
  1. {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/METADATA +22 -12
  2. maxapi_python-2.5.0.dist-info/RECORD +195 -0
  3. {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +8 -2
  5. pymax/_data/apk_fingerprints.json +374 -17
  6. pymax/_data/rootca_ssl_rsa2022.crt +33 -0
  7. pymax/api/auth/service.py +15 -2
  8. pymax/api/auth/types.py +3 -0
  9. pymax/api/bots/payloads.py +8 -0
  10. pymax/api/bots/service.py +13 -1
  11. pymax/api/chats/enums.py +1 -0
  12. pymax/api/chats/payloads.py +21 -1
  13. pymax/api/chats/service.py +41 -0
  14. pymax/api/facade.py +2 -0
  15. pymax/api/messages/__init__.py +1 -1
  16. pymax/api/messages/enums.py +1 -0
  17. pymax/api/messages/payloads.py +56 -4
  18. pymax/api/messages/service.py +405 -27
  19. pymax/api/response.py +1 -1
  20. pymax/api/stories/__init__.py +2 -0
  21. pymax/api/stories/enums.py +19 -0
  22. pymax/api/stories/payloads.py +64 -0
  23. pymax/api/stories/service.py +198 -0
  24. pymax/api/uploads/payloads.py +42 -1
  25. pymax/api/uploads/service.py +171 -69
  26. pymax/app.py +39 -8
  27. pymax/auth/__init__.py +2 -0
  28. pymax/auth/exceptions.py +9 -0
  29. pymax/auth/providers.py +6 -2
  30. pymax/auth/qr.py +11 -1
  31. pymax/auth/sms.py +11 -1
  32. pymax/base.py +146 -27
  33. pymax/client.py +39 -11
  34. pymax/client_web.py +15 -12
  35. pymax/config.py +101 -53
  36. pymax/di/__init__.py +2 -0
  37. pymax/di/context.py +18 -0
  38. pymax/di/resolver.py +38 -0
  39. pymax/dispatch/__init__.py +7 -0
  40. pymax/dispatch/dispatcher.py +143 -40
  41. pymax/dispatch/enums.py +1 -0
  42. pymax/dispatch/mapping.py +23 -3
  43. pymax/dispatch/middleware.py +32 -0
  44. pymax/dispatch/resolvers.py +4 -0
  45. pymax/dispatch/router.py +86 -34
  46. pymax/files/base.py +58 -1
  47. pymax/files/video.py +32 -40
  48. pymax/files/voice.py +18 -4
  49. pymax/filters/__init__.py +15 -0
  50. pymax/filters/base.py +218 -0
  51. pymax/filters/containers/__init__.py +2 -0
  52. pymax/filters/containers/chat.py +240 -0
  53. pymax/filters/containers/message.py +147 -0
  54. pymax/filters/filters.py +211 -0
  55. pymax/filters/magic.py +13 -0
  56. pymax/filters/presets.py +88 -0
  57. pymax/filters/refs.py +56 -0
  58. pymax/fingerprint/fingerprint.py +5 -22
  59. pymax/fingerprint/models.py +1 -0
  60. pymax/formatting/markdown.py +55 -1
  61. pymax/fsm/__init__.py +3 -0
  62. pymax/fsm/context.py +30 -0
  63. pymax/fsm/protocol.py +14 -0
  64. pymax/fsm/state.py +70 -0
  65. pymax/fsm/storage.py +151 -0
  66. pymax/infra/base.py +2 -0
  67. pymax/infra/bots.py +20 -0
  68. pymax/infra/chat.py +37 -5
  69. pymax/infra/message.py +204 -9
  70. pymax/infra/protocol.py +5 -1
  71. pymax/infra/stories.py +205 -0
  72. pymax/integrations/__init__.py +0 -0
  73. pymax/integrations/dishka.py +109 -0
  74. pymax/protocol/enums.py +2 -0
  75. pymax/protocol/tcp/payload.py +0 -2
  76. pymax/session/__init__.py +1 -1
  77. pymax/session/models.py +30 -0
  78. pymax/session/protocol.py +33 -7
  79. pymax/session/store.py +73 -5
  80. pymax/transport/tcp.py +11 -3
  81. pymax/types/domain/__init__.py +23 -1
  82. pymax/types/domain/attachments/__init__.py +1 -1
  83. pymax/types/domain/attachments/call.py +7 -1
  84. pymax/types/domain/attachments/contact.py +3 -3
  85. pymax/types/domain/attachments/control.py +3 -0
  86. pymax/types/domain/attachments/enums.py +16 -0
  87. pymax/types/domain/attachments/keyboards/__init__.py +1 -1
  88. pymax/types/domain/attachments/keyboards/enums.py +12 -0
  89. pymax/types/domain/attachments/keyboards/inline.py +74 -3
  90. pymax/types/domain/attachments/video.py +3 -3
  91. pymax/types/domain/callback.py +19 -0
  92. pymax/types/domain/comment.py +24 -0
  93. pymax/types/domain/enums.py +7 -0
  94. pymax/types/domain/message.py +158 -9
  95. pymax/types/domain/story.py +126 -0
  96. pymax/types/events/reaction.py +28 -4
  97. pymax/versions/__init__.py +2 -0
  98. pymax/versions/catalog.py +104 -0
  99. pymax/versions/exceptions.py +10 -0
  100. maxapi_python-2.4.0.dist-info/RECORD +0 -161
  101. {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -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 typing import TYPE_CHECKING, TypeAlias
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[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | Poll]:
83
- result: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | Poll] = []
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
- if not text and not attachments:
137
- logger.error("send_message failed: no text or attachments provided")
138
- raise ValueError("Either text or attachments must be provided")
139
-
140
- if text:
141
- clean_text, elements = Formatter.format_markdown(text)
142
- else:
143
- clean_text, elements = None, []
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=SendMessagePayloadMessage(
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
- response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
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=await self._upload_attachments(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: str,
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[str],
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: str,
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 | str, chat_id: int) -> ReadState:
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
@@ -60,7 +60,7 @@ def payload_item(
60
60
  if validation_type is None:
61
61
  return data
62
62
 
63
- return cast(Any, validation_type)(data)
63
+ return cast("Any", validation_type)(data)
64
64
 
65
65
 
66
66
  def require_payload_item(
@@ -0,0 +1,2 @@
1
+ from .enums import StoryStatsFilter
2
+ from .service import StoriesService
@@ -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