maxapi-python 2.4.1__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 (74) hide show
  1. {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/METADATA +17 -9
  2. {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/RECORD +74 -45
  3. {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +5 -1
  5. pymax/_data/apk_fingerprints.json +272 -0
  6. pymax/api/auth/service.py +1 -0
  7. pymax/api/auth/types.py +3 -0
  8. pymax/api/bots/payloads.py +8 -0
  9. pymax/api/bots/service.py +13 -1
  10. pymax/api/chats/enums.py +1 -0
  11. pymax/api/chats/payloads.py +19 -0
  12. pymax/api/chats/service.py +34 -0
  13. pymax/api/facade.py +2 -0
  14. pymax/api/messages/enums.py +1 -0
  15. pymax/api/messages/payloads.py +47 -1
  16. pymax/api/messages/service.py +308 -24
  17. pymax/api/response.py +1 -1
  18. pymax/api/stories/__init__.py +2 -0
  19. pymax/api/stories/enums.py +19 -0
  20. pymax/api/stories/payloads.py +64 -0
  21. pymax/api/stories/service.py +198 -0
  22. pymax/api/uploads/payloads.py +10 -1
  23. pymax/api/uploads/service.py +94 -7
  24. pymax/app.py +16 -4
  25. pymax/auth/qr.py +11 -1
  26. pymax/base.py +70 -25
  27. pymax/client.py +0 -1
  28. pymax/config.py +38 -32
  29. pymax/di/__init__.py +2 -0
  30. pymax/di/context.py +18 -0
  31. pymax/di/resolver.py +38 -0
  32. pymax/dispatch/__init__.py +7 -0
  33. pymax/dispatch/dispatcher.py +143 -40
  34. pymax/dispatch/enums.py +1 -0
  35. pymax/dispatch/mapping.py +23 -3
  36. pymax/dispatch/middleware.py +32 -0
  37. pymax/dispatch/resolvers.py +4 -0
  38. pymax/dispatch/router.py +86 -34
  39. pymax/filters/__init__.py +15 -0
  40. pymax/filters/base.py +218 -0
  41. pymax/filters/containers/__init__.py +2 -0
  42. pymax/filters/containers/chat.py +240 -0
  43. pymax/filters/containers/message.py +147 -0
  44. pymax/filters/filters.py +211 -0
  45. pymax/filters/magic.py +13 -0
  46. pymax/filters/presets.py +88 -0
  47. pymax/filters/refs.py +56 -0
  48. pymax/formatting/markdown.py +55 -1
  49. pymax/fsm/__init__.py +3 -0
  50. pymax/fsm/context.py +30 -0
  51. pymax/fsm/protocol.py +14 -0
  52. pymax/fsm/state.py +70 -0
  53. pymax/fsm/storage.py +151 -0
  54. pymax/infra/base.py +2 -0
  55. pymax/infra/bots.py +20 -0
  56. pymax/infra/chat.py +28 -0
  57. pymax/infra/message.py +195 -6
  58. pymax/infra/stories.py +205 -0
  59. pymax/integrations/__init__.py +0 -0
  60. pymax/integrations/dishka.py +109 -0
  61. pymax/protocol/enums.py +2 -0
  62. pymax/protocol/tcp/payload.py +0 -2
  63. pymax/types/domain/__init__.py +23 -1
  64. pymax/types/domain/attachments/__init__.py +1 -1
  65. pymax/types/domain/attachments/keyboards/__init__.py +1 -1
  66. pymax/types/domain/attachments/keyboards/enums.py +12 -0
  67. pymax/types/domain/attachments/keyboards/inline.py +74 -3
  68. pymax/types/domain/callback.py +19 -0
  69. pymax/types/domain/comment.py +24 -0
  70. pymax/types/domain/message.py +91 -22
  71. pymax/types/domain/story.py +126 -0
  72. pymax/types/events/reaction.py +28 -4
  73. pymax/versions/catalog.py +1 -1
  74. {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,78 @@
1
- from typing import Any, Literal
1
+ from typing import Annotated, Literal, TypeAlias
2
2
 
3
3
  from pydantic import Field
4
4
 
5
5
  from pymax.types.domain.attachments import AttachmentType
6
6
  from pymax.types.domain.base import CamelModel
7
7
 
8
+ from .enums import ButtonType
9
+
10
+
11
+ class BaseButton(CamelModel):
12
+ text: str
13
+
14
+
15
+ class CallbackButton(BaseButton):
16
+ type: Literal[ButtonType.CALLBACK] = ButtonType.CALLBACK
17
+ text: str
18
+ payload: str
19
+ intent: str
20
+
21
+
22
+ class LinkButton(BaseButton):
23
+ type: Literal[ButtonType.LINK] = ButtonType.LINK
24
+ url: str
25
+
26
+
27
+ class ChatButton(BaseButton):
28
+ type: Literal[ButtonType.CHAT] = ButtonType.CHAT
29
+ payload: str
30
+
31
+
32
+ class GeoLocationButton(BaseButton):
33
+ type: Literal[ButtonType.REQUEST_GEO_LOCATION] = ButtonType.REQUEST_GEO_LOCATION
34
+ quick: bool | None = None
35
+
36
+
37
+ class RequestContactButton(BaseButton):
38
+ type: Literal[ButtonType.REQUEST_CONTACT] = ButtonType.REQUEST_CONTACT
39
+
40
+
41
+ class OpenAppButton(BaseButton):
42
+ type: Literal[ButtonType.OPEN_APP] = ButtonType.OPEN_APP
43
+ contact_id: int | None = None
44
+ payload: str | None = None
45
+ web_app: str | None = None
46
+
47
+
48
+ class MessageButton(BaseButton):
49
+ type: Literal[ButtonType.MESSAGE] = ButtonType.MESSAGE
50
+
51
+
52
+ class ClipboardButton(BaseButton):
53
+ type: Literal[ButtonType.CLIPBOARD] = ButtonType.CLIPBOARD
54
+ payload: str
55
+
56
+
57
+ Button: TypeAlias = Annotated[
58
+ CallbackButton
59
+ | ChatButton
60
+ | LinkButton
61
+ | RequestContactButton
62
+ | GeoLocationButton
63
+ | OpenAppButton
64
+ | MessageButton
65
+ | ClipboardButton,
66
+ Field(discriminator="type"),
67
+ ]
68
+ ButtonRow: TypeAlias = list[Button]
69
+
70
+
71
+ class InlineKeyboard(CamelModel):
72
+ """Inline-клавиатура со списком строк кнопок в ``buttons``."""
73
+
74
+ buttons: list[ButtonRow]
75
+
8
76
 
9
77
  class InlineKeyboardAttachment(CamelModel):
10
78
  """Вложение inline-клавиатуры.
@@ -12,8 +80,11 @@ class InlineKeyboardAttachment(CamelModel):
12
80
  :ivar type: Тип вложения.
13
81
  :vartype type: Literal[AttachmentType.INLINE_KEYBOARD]
14
82
  :ivar keyboard: Данные inline-клавиатуры.
15
- :vartype keyboard: dict[str, Any]
83
+ :vartype keyboard: InlineKeyboard
84
+ :ivar callback_id: ID клавиатуры для ``client.send_callback()``.
85
+ :vartype callback_id: str
16
86
  """
17
87
 
18
88
  type: Literal[AttachmentType.INLINE_KEYBOARD] = Field(alias="_type")
19
- keyboard: dict[str, Any]
89
+ keyboard: InlineKeyboard
90
+ callback_id: str
@@ -0,0 +1,19 @@
1
+ from .base import CamelModel
2
+ from .chat import Chat
3
+ from .message import Message
4
+
5
+
6
+ class CallbackResponse(CamelModel):
7
+ """Результат нажатия inline-кнопки бота.
8
+
9
+ ``success`` показывает результат операции. Необязательные ``message`` и
10
+ ``chat`` содержат дополнительные данные ответа Max и поддерживают методы
11
+ действий, если ответ получен через клиент.
12
+ """
13
+
14
+ success: bool
15
+ unread: int
16
+ mark: int
17
+ message: Message | None = None
18
+ chat: Chat | None = None
19
+ chat_access_token: str | None = None
@@ -0,0 +1,24 @@
1
+ from .base import CamelModel
2
+
3
+
4
+ class CommentsInfo(CamelModel):
5
+ """Счетчики комментариев поста.
6
+
7
+ :ivar total_count: Общее количество комментариев.
8
+ :vartype total_count: int
9
+ """
10
+
11
+ total_count: int = 0
12
+
13
+
14
+ class CommentsInfoUpdate(CamelModel):
15
+ """Обновление счетчиков комментариев поста.
16
+
17
+ :ivar post_id: ID поста.
18
+ :vartype post_id: int
19
+ :ivar comments_info: Счетчики комментариев поста, если Max их прислал.
20
+ :vartype comments_info: CommentsInfo | None
21
+ """
22
+
23
+ post_id: int
24
+ comments_info: CommentsInfo | None
@@ -50,6 +50,11 @@ SendAttachments: TypeAlias = Sequence[SendAttachment] | None
50
50
  Link: TypeAlias = Annotated["ReplyLink | ForwardLink", Field(discriminator="type")]
51
51
 
52
52
 
53
+ class MessageLink(CamelModel):
54
+ message: Message
55
+ chat_id: int
56
+
57
+
53
58
  class ReactionCounter(CamelModel):
54
59
  """Счетчик одной реакции на сообщение.
55
60
 
@@ -98,7 +103,7 @@ class DelayedAttributes(CamelModel):
98
103
  notify_opponents: bool
99
104
 
100
105
 
101
- class ReplyLink(CamelModel):
106
+ class ReplyLink(MessageLink):
102
107
  """Источник сообщения, отправленного как reply.
103
108
 
104
109
  :ivar message: Сообщение, на которое ответили.
@@ -112,7 +117,7 @@ class ReplyLink(CamelModel):
112
117
  chat_id: int
113
118
 
114
119
 
115
- class ForwardLink(CamelModel):
120
+ class ForwardLink(MessageLink):
116
121
  """Источник пересланного сообщения.
117
122
 
118
123
  Помимо исходного сообщения содержит доступные в payload-е имя, ссылку,
@@ -134,24 +139,6 @@ class ForwardLink(CamelModel):
134
139
  chat_icon_url: str | None = None
135
140
 
136
141
 
137
- """
138
- "link": {
139
- "type": "FORWARD",
140
- "message": {
141
- "id": "116641336752745888",
142
- "time": 1779805553478,
143
- "type": "CHANNEL",
144
- "text": "Добро пожаловать на наше шоу!",
145
- "attaches": []
146
- },
147
- "chatId": -75183661482741,
148
- "chatName": "Max Bounty Test",
149
- "chatLink": "https://max.ru/join/htlT5aTtlpAgcno_hc93lLM44Dxe45HvOEBqx22lKms",
150
- "chatAccessType": "PRIVATE"
151
- }
152
- """
153
-
154
-
155
142
  class Message(CamelModel):
156
143
  """Сообщение Max с методами для действий над ним.
157
144
 
@@ -181,6 +168,8 @@ class Message(CamelModel):
181
168
  :vartype id: int
182
169
  :ivar chat_id: ID чата, если он есть в данных сообщения.
183
170
  :vartype chat_id: int | None
171
+ :ivar post_id: ID поста канала, если сообщение является комментарием.
172
+ :vartype post_id: int | None
184
173
  :ivar sender: ID отправителя.
185
174
  :vartype sender: int | None
186
175
  :ivar text: Текст сообщения.
@@ -222,6 +211,7 @@ class Message(CamelModel):
222
211
 
223
212
  id: int
224
213
  chat_id: int | None = None
214
+ post_id: int | None = None
225
215
  sender: int | None = None
226
216
  text: str = ""
227
217
  time: int
@@ -261,7 +251,7 @@ class Message(CamelModel):
261
251
  notify: bool = True,
262
252
  send_at: DateTimeUnion | None = None,
263
253
  ) -> Message:
264
- """Отправляет ответ на это сообщение в тот же чат.
254
+ """Отправляет ответ в тот же чат или ветку комментариев.
265
255
 
266
256
  :param text: Текст сообщения.
267
257
  :type text: str | None
@@ -276,9 +266,29 @@ class Message(CamelModel):
276
266
  :rtype: Message
277
267
  :raises RuntimeError: Если сообщение не привязано к сервису или не
278
268
  содержит ``chat_id``.
269
+ :raises ValueError: Если для комментария задана отложенная отправка.
279
270
  """
280
271
  actions, chat_id = self._bound()
281
272
 
273
+ if self.post_id is not None:
274
+ if send_at:
275
+ raise ValueError("Scheduled comments are not supported")
276
+
277
+ message = await actions.send_comment(
278
+ chat_id=chat_id,
279
+ post_id=self.post_id,
280
+ text=text,
281
+ reply_to=self.id,
282
+ attachments=attachments,
283
+ notify=notify,
284
+ )
285
+ if message.chat_id is None:
286
+ message.chat_id = chat_id
287
+ if message.post_id is None:
288
+ message.post_id = self.post_id
289
+
290
+ return message
291
+
282
292
  return await actions.send_message(
283
293
  chat_id=chat_id,
284
294
  text=text,
@@ -297,7 +307,7 @@ class Message(CamelModel):
297
307
  notify: bool = True,
298
308
  send_at: DateTimeUnion | None = None,
299
309
  ) -> Message:
300
- """Отправляет сообщение в тот же чат.
310
+ """Отправляет сообщение в тот же чат или ветку комментариев.
301
311
 
302
312
  :param text: Текст сообщения.
303
313
  :type text: str | None
@@ -314,9 +324,29 @@ class Message(CamelModel):
314
324
  :rtype: Message
315
325
  :raises RuntimeError: Если сообщение не привязано к сервису или не
316
326
  содержит ``chat_id``.
327
+ :raises ValueError: Если для комментария задана отложенная отправка.
317
328
  """
318
329
  actions, chat_id = self._bound()
319
330
 
331
+ if self.post_id is not None:
332
+ if send_at:
333
+ raise ValueError("Scheduled comments are not supported")
334
+
335
+ message = await actions.send_comment(
336
+ chat_id=chat_id,
337
+ post_id=self.post_id,
338
+ text=text,
339
+ reply_to=reply_to,
340
+ attachments=attachments,
341
+ notify=notify,
342
+ )
343
+ if message.chat_id is None:
344
+ message.chat_id = chat_id
345
+ if message.post_id is None:
346
+ message.post_id = self.post_id
347
+
348
+ return message
349
+
320
350
  return await actions.send_message(
321
351
  chat_id=chat_id,
322
352
  text=text,
@@ -388,6 +418,21 @@ class Message(CamelModel):
388
418
  """
389
419
  actions, chat_id = self._bound()
390
420
 
421
+ if self.post_id is not None:
422
+ message = await actions.edit_comment(
423
+ chat_id=chat_id,
424
+ post_id=self.post_id,
425
+ message_id=self.id,
426
+ text=text,
427
+ attachments=attachments,
428
+ )
429
+ if message.chat_id is None:
430
+ message.chat_id = chat_id
431
+ if message.post_id is None:
432
+ message.post_id = self.post_id
433
+
434
+ return message
435
+
391
436
  return await actions.edit_message(
392
437
  chat_id=chat_id,
393
438
  message_id=self.id,
@@ -407,6 +452,14 @@ class Message(CamelModel):
407
452
  """
408
453
  actions, chat_id = self._bound()
409
454
 
455
+ if self.post_id is not None:
456
+ return await actions.delete_comment(
457
+ chat_id=chat_id,
458
+ post_id=self.post_id,
459
+ message_ids=[self.id],
460
+ for_me=for_me,
461
+ )
462
+
410
463
  return await actions.delete_message(
411
464
  chat_id=chat_id,
412
465
  message_ids=[self.id],
@@ -440,6 +493,14 @@ class Message(CamelModel):
440
493
  """
441
494
  actions, chat_id = self._bound()
442
495
 
496
+ if self.post_id is not None:
497
+ return await actions.add_comment_reaction(
498
+ chat_id=chat_id,
499
+ post_id=self.post_id,
500
+ message_id=self.id,
501
+ reaction=reaction,
502
+ )
503
+
443
504
  return await actions.add_reaction(
444
505
  chat_id=chat_id,
445
506
  message_id=self.id,
@@ -456,6 +517,13 @@ class Message(CamelModel):
456
517
  """
457
518
  actions, chat_id = self._bound()
458
519
 
520
+ if self.post_id is not None:
521
+ return await actions.remove_comment_reaction(
522
+ chat_id=chat_id,
523
+ post_id=self.post_id,
524
+ message_id=self.id,
525
+ )
526
+
459
527
  return await actions.remove_reaction(
460
528
  chat_id=chat_id,
461
529
  message_id=self.id,
@@ -507,6 +575,7 @@ class Message(CamelModel):
507
575
  return {
508
576
  **message,
509
577
  "chatId": value.get("chatId"),
578
+ "postId": value.get("postId"),
510
579
  "prevMessageId": value.get("prevMessageId"),
511
580
  "ttl": value.get("ttl"),
512
581
  "unread": value.get("unread"),
@@ -0,0 +1,126 @@
1
+ from typing import Annotated, Literal, TypeAlias
2
+
3
+ from pydantic import Field
4
+
5
+ from .attachments.enums import AttachmentType
6
+ from .base import CamelModel
7
+
8
+
9
+ class StoryCoordinates(CamelModel):
10
+ """Положение слоя сторис в долях ширины и высоты кадра."""
11
+
12
+ x: float
13
+ y: float
14
+ w: float
15
+ h: float
16
+ rotation: float = 0
17
+
18
+
19
+ class StoryClickableLink(CamelModel):
20
+ """Ссылка слоя сторис и результат ее проверки сервером."""
21
+
22
+ url: str
23
+ check_result: int | None = None
24
+
25
+
26
+ class StoryLayer(CamelModel):
27
+ """Слой сторис со ссылкой и координатами."""
28
+
29
+ type: int = 0
30
+ coordinates: StoryCoordinates
31
+ clickable_link: StoryClickableLink
32
+
33
+
34
+ class StoryPhotoMedia(CamelModel):
35
+ """Фото сторис, полученное от Max, без обязательного токена загрузки."""
36
+
37
+ base_url: str
38
+ height: int
39
+ width: int
40
+ photo_id: int
41
+ preview_data: bytes | None = None
42
+ type: Literal[AttachmentType.PHOTO] = Field(alias="_type")
43
+
44
+
45
+ class StoryVideoMedia(CamelModel):
46
+ """Видео сторис, полученное от Max.
47
+
48
+ В отличие от ``VideoAttachment``, не требует токен загрузки и
49
+ ``video_type``, которые используются при публикации.
50
+ """
51
+
52
+ height: int
53
+ width: int
54
+ video_id: int
55
+ duration: int | None = None
56
+ preview_data: bytes | None = None
57
+ type: Literal[AttachmentType.VIDEO] = Field(alias="_type")
58
+ thumbnail: str
59
+
60
+
61
+ StoryMedia: TypeAlias = Annotated[
62
+ StoryPhotoMedia | StoryVideoMedia,
63
+ Field(discriminator="type"),
64
+ ]
65
+
66
+
67
+ class Story(CamelModel):
68
+ """Сторис, возвращенная после публикации или при чтении.
69
+
70
+ :ivar id: ID сторис.
71
+ :ivar cid: Идентификатор публикации, сформированный клиентом.
72
+ :ivar media: Фото или видео сторис.
73
+ :ivar layers: Слои поверх кадра.
74
+ :ivar settings: Настройки сторис в формате Max.
75
+ :ivar expiration: Время жизни сторис в миллисекундах.
76
+ """
77
+
78
+ id: int
79
+ cid: int | None = None
80
+ media: StoryMedia
81
+ layers: list[StoryLayer] = Field(default_factory=list)
82
+ settings: int
83
+ expiration: int
84
+
85
+
86
+ class StoryOwner(CamelModel):
87
+ """Владелец сторис.
88
+
89
+ :ivar owner_id: ID владельца, а не отдельной сторис.
90
+ :ivar type: Числовой тип владельца из Max. ``0`` — пользователь.
91
+ """
92
+
93
+ owner_id: int
94
+ type: int = 0
95
+
96
+
97
+ class StoryPreview(CamelModel):
98
+ """Сводная информация о сторис владельца в ленте и обновлениях.
99
+
100
+ ID отдельных сторис в превью нет. Для их получения передайте
101
+ ``owner.owner_id`` и ``owner.type`` в ``client.get_stories()``.
102
+
103
+ :ivar owner: Владелец сторис.
104
+ :ivar total_count: Общее количество сторис владельца.
105
+ :ivar read_count: Количество просмотренных сторис владельца.
106
+ """
107
+
108
+ owner: StoryOwner
109
+ total_count: int = 0
110
+ read_count: int = 0
111
+ last_story_expiration_time: int = 0
112
+ settings: int = 0
113
+ update_time: int = 0
114
+
115
+
116
+ class StoryDetailedStats(CamelModel):
117
+ """Запись подробной статистики просмотра или реакции на сторис.
118
+
119
+ Типизировано только ``user_id``. Дополнительные поля доступны в
120
+ ``model_extra`` с исходными именами ответа Max, без преобразования
121
+ camelCase. Поле ``reaction``, если оно пришло, остается словарем.
122
+
123
+ :ivar user_id: ID пользователя.
124
+ """
125
+
126
+ user_id: int
@@ -1,3 +1,7 @@
1
+ from typing import Any
2
+
3
+ from pydantic import model_validator
4
+
1
5
  from pymax.types.domain.base import CamelModel
2
6
  from pymax.types.domain.message import ReactionCounter
3
7
 
@@ -6,16 +10,36 @@ class ReactionUpdateEvent(CamelModel):
6
10
  """Событие обновления реакций сообщения.
7
11
 
8
12
  :ivar message_id: ID сообщения.
9
- :vartype message_id: str
13
+ :vartype message_id: int
10
14
  :ivar chat_id: ID чата.
11
15
  :vartype chat_id: int
12
- :ivar counters: Счетчики реакций по типам.
13
- :vartype counters: list[ReactionCounter]
16
+ :ivar counters: Счетчики реакций по типам, если Max их прислал.
17
+ :vartype counters: list[ReactionCounter] | None
14
18
  :ivar total_count: Общее количество реакций.
15
19
  :vartype total_count: int
20
+ :ivar your_reaction: Реакция текущего пользователя, если она есть.
21
+ :vartype your_reaction: str | None
16
22
  """
17
23
 
18
- message_id: str
24
+ message_id: int
19
25
  chat_id: int
20
26
  counters: list[ReactionCounter] | None = None
21
27
  total_count: int = 0
28
+ your_reaction: str | None = None
29
+
30
+ @model_validator(mode="before")
31
+ @classmethod
32
+ def _unwrap_reaction_info(cls, value: Any) -> Any:
33
+ if not isinstance(value, dict):
34
+ return value
35
+
36
+ reaction_info = value.get("reactionInfo")
37
+ if not isinstance(reaction_info, dict):
38
+ return value
39
+
40
+ data = value.copy()
41
+ for field in ("counters", "totalCount", "yourReaction"):
42
+ if field in reaction_info:
43
+ data[field] = reaction_info[field]
44
+
45
+ return data
pymax/versions/catalog.py CHANGED
@@ -21,7 +21,7 @@ class VersionCatalog:
21
21
  remote: Загружать ли удаленные версии при :meth:`load`.
22
22
  """
23
23
 
24
- RECOMMENDED_APP_VERSION: str = "26.25.0"
24
+ RECOMMENDED_APP_VERSION: str = "26.37.1"
25
25
  DEFAULT_REMOTE_URL: str = "https://hashes.pymax.org/versions.json"
26
26
 
27
27
  def __init__(self, remote: bool = False) -> None: