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
pymax/fsm/storage.py ADDED
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from contextlib import asynccontextmanager
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from typing import TYPE_CHECKING, Any, AsyncGenerator
8
+
9
+ from pymax.fsm.context import FSMContext, FSMKey
10
+ from pymax.types.domain.message import Message
11
+
12
+ from .protocol import FSMStorage
13
+ from .state import State
14
+
15
+ if TYPE_CHECKING:
16
+ from pymax.base import BaseClient
17
+ from pymax.dispatch import DispatchEvent
18
+
19
+
20
+ class InMemoryFSMStorage:
21
+ def __init__(self) -> None:
22
+ self.records: dict[FSMKey, InMemoryFSM] = {}
23
+ self.locks: dict[FSMKey, asyncio.Lock] = {}
24
+
25
+ def resolve_key(
26
+ self,
27
+ client: BaseClient,
28
+ event: DispatchEvent,
29
+ ) -> FSMKey | None:
30
+ if not isinstance(event, Message):
31
+ return None
32
+
33
+ if event.chat_id is None or event.sender is None:
34
+ return None
35
+
36
+ return FSMKey(
37
+ client_id=hash(client),
38
+ user_id=event.sender,
39
+ chat_id=event.chat_id,
40
+ )
41
+
42
+ def get_context(
43
+ self,
44
+ client: BaseClient,
45
+ event: Message,
46
+ ) -> FSMContext | None:
47
+ if not event.chat_id or not event.sender:
48
+ return None
49
+
50
+ key = self.resolve_key(client, event)
51
+
52
+ if not key:
53
+ return None
54
+
55
+ record = self.records.get(key)
56
+ if record is None:
57
+ record = InMemoryFSM()
58
+ self.records[key] = record
59
+
60
+ return FSMContext(storage=record, key=key)
61
+
62
+ @asynccontextmanager
63
+ async def isolate(
64
+ self,
65
+ client: BaseClient,
66
+ event: DispatchEvent,
67
+ ) -> AsyncGenerator[None]:
68
+ key = self.resolve_key(client, event)
69
+
70
+ if key is None:
71
+ yield
72
+ return
73
+
74
+ lock = self.locks.setdefault(key, asyncio.Lock())
75
+
76
+ async with lock:
77
+ yield
78
+
79
+
80
+ @dataclass
81
+ class FsmState:
82
+ value: str | None = None
83
+ created_at: datetime = datetime.now()
84
+ ttl: int | None = None
85
+
86
+
87
+ class InMemoryFSM(FSMStorage):
88
+ """Состояние и данные одного FSM-контекста в памяти процесса.
89
+
90
+ В обработчике это хранилище доступно как ``ctx.storage``.
91
+ """
92
+
93
+ def __init__(self) -> None:
94
+ self.state: FsmState = FsmState()
95
+ self.data: dict[str, Any] = {}
96
+ self.lock = asyncio.Lock()
97
+
98
+ async def get_state(self) -> str | None:
99
+ """Возвращает имя состояния или ``None``, если оно не задано или истек TTL.
100
+
101
+ Истечение TTL сбрасывает только состояние, сохраняя данные контекста.
102
+ """
103
+ async with self.lock:
104
+ if self.state.ttl is None:
105
+ return self.state.value
106
+
107
+ expires_at = self.state.created_at.timestamp() + self.state.ttl
108
+
109
+ if datetime.now().timestamp() >= expires_at:
110
+ self.state = FsmState()
111
+ return None
112
+
113
+ return self.state.value
114
+
115
+ async def set_state(self, state: str | State | None, ttl: int | None = None) -> None:
116
+ """Устанавливает состояние, сохраняя данные контекста.
117
+
118
+ Args:
119
+ state: Состояние, его строковое имя или ``None`` для сброса.
120
+ ttl: Срок действия в секундах с момента установки.
121
+ ``None`` означает отсутствие срока действия.
122
+ """
123
+ async with self.lock:
124
+ if state is None:
125
+ self.state = FsmState()
126
+ return
127
+
128
+ self.state.value = str(state)
129
+ self.state.ttl = ttl
130
+ self.state.created_at = datetime.now()
131
+
132
+ async def get_data(self) -> dict[str, Any]:
133
+ """Возвращает поверхностную копию словаря данных контекста."""
134
+ async with self.lock:
135
+ return self.data.copy()
136
+
137
+ async def set_data(self, key: str, value: Any) -> None:
138
+ """Записывает одно значение, сохраняя остальные ключи данных."""
139
+ async with self.lock:
140
+ self.data[key] = value
141
+
142
+ async def update_data(self, **data: Any) -> None:
143
+ """Добавляет или перезаписывает переданные ключи данных."""
144
+ async with self.lock:
145
+ self.data.update(data)
146
+
147
+ async def clear(self) -> None:
148
+ """Сбрасывает состояние и удаляет все данные текущего контекста."""
149
+ async with self.lock:
150
+ self.state = FsmState()
151
+ self.data.clear()
pymax/infra/base.py CHANGED
@@ -3,6 +3,7 @@ from .bots import BotsMixin
3
3
  from .chat import ChatMixin
4
4
  from .message import MessageMixin
5
5
  from .self import SelfMixin
6
+ from .stories import StoriesMixin
6
7
  from .user import UserMixin
7
8
 
8
9
 
@@ -13,5 +14,6 @@ class BaseMixin(
13
14
  MessageMixin,
14
15
  BotsMixin,
15
16
  AuthMixin,
17
+ StoriesMixin,
16
18
  ):
17
19
  """Собирает публичные API-методы клиента."""
pymax/infra/bots.py CHANGED
@@ -1,3 +1,4 @@
1
+ from pymax.types import ButtonType, CallbackResponse
1
2
  from pymax.types.domain import InitData
2
3
 
3
4
  from .protocol import IClientProtocol
@@ -31,3 +32,22 @@ class BotsMixin(IClientProtocol):
31
32
  chat_id=chat_id,
32
33
  start_param=start_param,
33
34
  )
35
+
36
+ async def send_callback(
37
+ self,
38
+ callback_id: str,
39
+ type: ButtonType,
40
+ payload: str | None = None,
41
+ ) -> CallbackResponse:
42
+ """Передает нажатие inline-кнопки бота.
43
+
44
+ Args:
45
+ callback_id: ID из ``InlineKeyboardAttachment.callback_id``.
46
+ type: Тип нажатой кнопки.
47
+ payload: Данные кнопки, если они есть.
48
+
49
+ Returns:
50
+ Результат нажатия. ``message`` и ``chat`` содержатся в нем,
51
+ только если Max вернул эти объекты.
52
+ """
53
+ return await self._app.api.bots.send_callback(callback_id, type, payload)
pymax/infra/chat.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from pymax.api.chats import ChannelPermissions
2
+ from pymax.files import Photo
2
3
  from pymax.types import Chat, Member, Message
3
4
 
4
5
  from .protocol import IClientProtocol
@@ -131,20 +132,23 @@ class ChatMixin(IClientProtocol):
131
132
  async def change_group_profile(
132
133
  self,
133
134
  chat_id: int,
134
- name: str | None,
135
+ name: str | None = None,
135
136
  description: str | None = None,
137
+ photo: Photo | None = None,
136
138
  ) -> None:
137
- """Обновляет название и описание группы.
139
+ """Обновляет название, описание или фотографию группы.
140
+
141
+ ``None`` исключает соответствующее поле из запроса. Фотография
142
+ автоматически загружается перед обновлением профиля.
138
143
 
139
144
  Args:
140
145
  chat_id: ID группы.
141
146
  name: Новое название.
142
147
  description: Новое описание.
148
+ photo: Новая фотография группы.
143
149
  """
144
150
  await self._app.api.chats.change_group_profile(
145
- chat_id=chat_id,
146
- name=name,
147
- description=description,
151
+ chat_id=chat_id, name=name, description=description, photo=photo
148
152
  )
149
153
 
150
154
  async def join_group(self, link: str) -> Chat:
@@ -410,3 +414,31 @@ class ChatMixin(IClientProtocol):
410
414
  permissions: Непустой список прав администратора.
411
415
  """
412
416
  return await self._app.api.chats.add_admin(chat_id, user_id, permissions)
417
+
418
+ async def set_channel_comments(self, chat_id: int, enabled: bool) -> None:
419
+ """Включает или выключает комментарии канала."""
420
+ return await self._app.api.chats.set_channel_comments(
421
+ chat_id=chat_id,
422
+ enabled=enabled,
423
+ )
424
+
425
+ async def block_comment_author(
426
+ self,
427
+ chat_id: int,
428
+ post_id: int,
429
+ user_ids: list[int],
430
+ message_id: int,
431
+ clean_msg_period: int = 0,
432
+ ) -> bool:
433
+ """Блокирует авторов в комментариях.
434
+
435
+ ``message_id`` — ID комментария для модерации.
436
+ ``clean_msg_period=0`` сохраняет комментарии, ``-1`` также удаляет их.
437
+ """
438
+ return await self._app.api.chats.block_comment_author(
439
+ chat_id=chat_id,
440
+ post_id=post_id,
441
+ user_ids=user_ids,
442
+ message_id=message_id,
443
+ clean_msg_period=clean_msg_period,
444
+ )
pymax/infra/message.py CHANGED
@@ -1,6 +1,8 @@
1
+ from pymax.api.messages import DateTimeUnion
1
2
  from pymax.api.messages.enums import ItemType
2
3
  from pymax.api.messages.service import SendAttachments
3
4
  from pymax.types import (
5
+ CommentsInfoUpdate,
4
6
  FileRequest,
5
7
  Message,
6
8
  PollState,
@@ -23,6 +25,7 @@ class MessageMixin(IClientProtocol):
23
25
  attachments: SendAttachments = None,
24
26
  *,
25
27
  notify: bool = True,
28
+ send_at: DateTimeUnion | None = None,
26
29
  ) -> Message:
27
30
  """Отправляет сообщение в чат.
28
31
 
@@ -32,6 +35,9 @@ class MessageMixin(IClientProtocol):
32
35
  reply_to: ID сообщения для ответа.
33
36
  attachments: Файлы, фотографии, видео или опросы для отправки.
34
37
  notify: Отправить ли получателям push-уведомление.
38
+ send_at: Время отложенной отправки: ``datetime`` для абсолютного
39
+ времени, ``timedelta`` для задержки или Unix time в секундах
40
+ как ``int``. ``None`` и ``0`` отправляют сообщение сразу.
35
41
 
36
42
  Returns:
37
43
  Отправленное сообщение.
@@ -45,6 +51,7 @@ class MessageMixin(IClientProtocol):
45
51
  reply_to,
46
52
  attachments,
47
53
  notify=notify,
54
+ send_at=send_at,
48
55
  )
49
56
 
50
57
  async def get_message(
@@ -272,7 +279,7 @@ class MessageMixin(IClientProtocol):
272
279
  async def add_reaction(
273
280
  self,
274
281
  chat_id: int,
275
- message_id: str,
282
+ message_id: int,
276
283
  reaction: str,
277
284
  ) -> ReactionInfo | None:
278
285
  """Добавляет реакцию к сообщению.
@@ -294,7 +301,7 @@ class MessageMixin(IClientProtocol):
294
301
  async def get_reactions(
295
302
  self,
296
303
  chat_id: int,
297
- message_ids: list[str],
304
+ message_ids: list[int],
298
305
  ) -> dict[str, ReactionInfo] | None:
299
306
  """Возвращает реакции для нескольких сообщений.
300
307
 
@@ -314,7 +321,7 @@ class MessageMixin(IClientProtocol):
314
321
  async def remove_reaction(
315
322
  self,
316
323
  chat_id: int,
317
- message_id: str,
324
+ message_id: int,
318
325
  ) -> ReactionInfo | None:
319
326
  """Удаляет свою реакцию с сообщения.
320
327
 
@@ -330,15 +337,11 @@ class MessageMixin(IClientProtocol):
330
337
  message_id=message_id,
331
338
  )
332
339
 
333
- async def read_message(self, message_id: int | str, chat_id: int) -> ReadState:
340
+ async def read_message(self, message_id: int, chat_id: int) -> ReadState:
334
341
  """Отмечает сообщение как прочитанное.
335
342
 
336
- У Max различается wire-формат ``message_id`` для отметки прочтения:
337
- TCP-клиент ожидает ``int``, WebSocket-клиент - ``str``.
338
-
339
343
  Args:
340
- message_id: ID сообщения. Передавайте ``int`` для ``Client`` и
341
- ``str`` для ``WebClient``.
344
+ message_id: ID сообщения как ``int`` для ``Client`` и ``WebClient``.
342
345
  chat_id: ID чата.
343
346
 
344
347
  Returns:
@@ -373,3 +376,195 @@ class MessageMixin(IClientProtocol):
373
376
  poll_id=poll_id,
374
377
  answer_ids=answer_ids,
375
378
  )
379
+
380
+ async def send_comment(
381
+ self,
382
+ chat_id: int,
383
+ post_id: int,
384
+ text: str | None = None,
385
+ reply_to: int | None = None,
386
+ attachments: SendAttachments = None,
387
+ *,
388
+ notify: bool = True,
389
+ ) -> Message:
390
+ """Отправляет комментарий к посту канала.
391
+
392
+ Args:
393
+ chat_id: ID канала.
394
+ post_id: ID поста в канале.
395
+ text: Текст комментария с поддержкой Markdown.
396
+ reply_to: ID комментария, на который нужно ответить.
397
+ attachments: Вложения комментария.
398
+ notify: Отправить уведомление о комментарии.
399
+
400
+ Returns:
401
+ Опубликованный комментарий как ``Message``.
402
+ """
403
+ return await self._app.api.messages.send_comment(
404
+ chat_id=chat_id,
405
+ post_id=post_id,
406
+ text=text,
407
+ reply_to=reply_to,
408
+ attachments=attachments,
409
+ notify=notify,
410
+ )
411
+
412
+ async def fetch_comments(
413
+ self,
414
+ chat_id: int,
415
+ post_id: int,
416
+ from_: int | None = None,
417
+ backward: int = 30,
418
+ get_messages: bool = True,
419
+ forward: int = 0,
420
+ ) -> list[Message]:
421
+ """Возвращает страницу комментариев поста канала.
422
+
423
+ ``chat_id`` задает канал, ``post_id`` — пост внутри него.
424
+ ``backward`` и ``forward`` задают количество комментариев по сторонам
425
+ от ``from_``. При ``from_=None`` используется начальная позиция Max.
426
+ """
427
+ return await self._app.api.messages.fetch_comments(
428
+ chat_id=chat_id,
429
+ post_id=post_id,
430
+ from_=from_,
431
+ backward=backward,
432
+ get_messages=get_messages,
433
+ forward=forward,
434
+ )
435
+
436
+ async def get_comments(
437
+ self,
438
+ chat_id: int,
439
+ post_id: int,
440
+ message_ids: list[int],
441
+ ) -> list[Message]:
442
+ """Возвращает конкретные комментарии поста по их ID.
443
+
444
+ ``chat_id`` задает канал, ``post_id`` — пост, а ``message_ids`` —
445
+ ID запрашиваемых комментариев.
446
+ """
447
+ return await self._app.api.messages.get_comments(
448
+ chat_id=chat_id,
449
+ post_id=post_id,
450
+ message_ids=message_ids,
451
+ )
452
+
453
+ async def get_comment(
454
+ self,
455
+ chat_id: int,
456
+ post_id: int,
457
+ message_id: int,
458
+ ) -> Message | None:
459
+ """Возвращает комментарий по ID или ``None``, если он не найден."""
460
+ return await self._app.api.messages.get_comment(
461
+ chat_id=chat_id,
462
+ post_id=post_id,
463
+ message_id=message_id,
464
+ )
465
+
466
+ async def edit_comment(
467
+ self,
468
+ chat_id: int,
469
+ post_id: int,
470
+ message_id: int,
471
+ text: str | None = None,
472
+ attachments: SendAttachments = None,
473
+ ) -> Message:
474
+ """Редактирует текст и вложения комментария. Текст поддерживает markdown."""
475
+ return await self._app.api.messages.edit_comment(
476
+ chat_id=chat_id,
477
+ post_id=post_id,
478
+ message_id=message_id,
479
+ text=text,
480
+ attachments=attachments,
481
+ )
482
+
483
+ async def delete_comment(
484
+ self,
485
+ chat_id: int,
486
+ post_id: int,
487
+ message_ids: list[int],
488
+ for_me: bool = False,
489
+ ) -> bool:
490
+ """Удаляет комментарии поста. По умолчанию удаляет для всех."""
491
+ return await self._app.api.messages.delete_comment(
492
+ chat_id=chat_id,
493
+ post_id=post_id,
494
+ message_ids=message_ids,
495
+ for_me=for_me,
496
+ )
497
+
498
+ async def add_comment_reaction(
499
+ self,
500
+ chat_id: int,
501
+ post_id: int,
502
+ message_id: int,
503
+ reaction: str,
504
+ ) -> ReactionInfo | None:
505
+ """Добавляет или заменяет свою emoji-реакцию на комментарии."""
506
+ return await self._app.api.messages.add_comment_reaction(
507
+ chat_id=chat_id,
508
+ post_id=post_id,
509
+ message_id=message_id,
510
+ reaction=reaction,
511
+ )
512
+
513
+ async def remove_comment_reaction(
514
+ self,
515
+ chat_id: int,
516
+ post_id: int,
517
+ message_id: int,
518
+ ) -> ReactionInfo | None:
519
+ """Удаляет свою реакцию с комментария."""
520
+ return await self._app.api.messages.remove_comment_reaction(
521
+ chat_id=chat_id,
522
+ post_id=post_id,
523
+ message_id=message_id,
524
+ )
525
+
526
+ async def subscribe_comments(
527
+ self,
528
+ chat_id: int,
529
+ post_id: int,
530
+ subscribe: bool = True,
531
+ ) -> None:
532
+ """Подписывается на события ветки комментариев.
533
+
534
+ Для поддержания подписки повторяйте вызов раз в минуту.
535
+ ``subscribe=False`` отменяет подписку.
536
+ """
537
+ return await self._app.api.messages.subscribe_comments(
538
+ chat_id=chat_id,
539
+ post_id=post_id,
540
+ subscribe=subscribe,
541
+ )
542
+
543
+ async def get_comments_info(
544
+ self,
545
+ chat_id: int,
546
+ post_ids: list[int],
547
+ ) -> list[CommentsInfoUpdate]:
548
+ """Возвращает список обновлений счетчиков комментариев постов."""
549
+ return await self._app.api.messages.get_comments_info(
550
+ chat_id=chat_id,
551
+ post_ids=post_ids,
552
+ )
553
+
554
+ async def delete_user_comments(
555
+ self,
556
+ chat_id: int,
557
+ post_id: int,
558
+ user_id: int,
559
+ message_id: int,
560
+ ) -> bool:
561
+ """Удаляет комментарии автора в ветке указанного поста.
562
+
563
+ ``message_id`` — ID комментария этого автора.
564
+ """
565
+ return await self._app.api.messages.delete_user_comments(
566
+ chat_id=chat_id,
567
+ post_id=post_id,
568
+ user_id=user_id,
569
+ message_id=message_id,
570
+ )
pymax/infra/protocol.py CHANGED
@@ -6,4 +6,8 @@ from pymax.app import App
6
6
  class IClientProtocol(Protocol):
7
7
  """Описывает минимальный клиент, нужный infra-миксинам."""
8
8
 
9
- _app: App
9
+ @property
10
+ def _app(self) -> App: ...
11
+
12
+ @_app.setter
13
+ def _app(self, value: App) -> None: ...