maxapi-python 2.4.0__py3-none-any.whl → 2.4.1__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 (47) hide show
  1. {maxapi_python-2.4.0.dist-info → maxapi_python-2.4.1.dist-info}/METADATA +6 -4
  2. {maxapi_python-2.4.0.dist-info → maxapi_python-2.4.1.dist-info}/RECORD +47 -42
  3. {maxapi_python-2.4.0.dist-info → maxapi_python-2.4.1.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +4 -2
  5. pymax/_data/apk_fingerprints.json +102 -17
  6. pymax/_data/rootca_ssl_rsa2022.crt +33 -0
  7. pymax/api/auth/service.py +14 -2
  8. pymax/api/chats/payloads.py +2 -1
  9. pymax/api/chats/service.py +7 -0
  10. pymax/api/messages/__init__.py +1 -1
  11. pymax/api/messages/payloads.py +9 -3
  12. pymax/api/messages/service.py +106 -12
  13. pymax/api/uploads/payloads.py +33 -1
  14. pymax/api/uploads/service.py +77 -62
  15. pymax/app.py +23 -4
  16. pymax/auth/__init__.py +2 -0
  17. pymax/auth/exceptions.py +9 -0
  18. pymax/auth/providers.py +6 -2
  19. pymax/auth/sms.py +11 -1
  20. pymax/base.py +83 -9
  21. pymax/client.py +39 -10
  22. pymax/client_web.py +15 -12
  23. pymax/config.py +63 -21
  24. pymax/files/base.py +58 -1
  25. pymax/files/video.py +32 -40
  26. pymax/files/voice.py +18 -4
  27. pymax/fingerprint/fingerprint.py +5 -22
  28. pymax/fingerprint/models.py +1 -0
  29. pymax/infra/chat.py +9 -5
  30. pymax/infra/message.py +9 -3
  31. pymax/infra/protocol.py +5 -1
  32. pymax/session/__init__.py +1 -1
  33. pymax/session/models.py +30 -0
  34. pymax/session/protocol.py +33 -7
  35. pymax/session/store.py +73 -5
  36. pymax/transport/tcp.py +11 -3
  37. pymax/types/domain/attachments/call.py +7 -1
  38. pymax/types/domain/attachments/contact.py +3 -3
  39. pymax/types/domain/attachments/control.py +3 -0
  40. pymax/types/domain/attachments/enums.py +16 -0
  41. pymax/types/domain/attachments/video.py +3 -3
  42. pymax/types/domain/enums.py +7 -0
  43. pymax/types/domain/message.py +87 -7
  44. pymax/versions/__init__.py +2 -0
  45. pymax/versions/catalog.py +104 -0
  46. pymax/versions/exceptions.py +10 -0
  47. {maxapi_python-2.4.0.dist-info → maxapi_python-2.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Sequence
4
- from typing import TYPE_CHECKING, Annotated, Any, TypeAlias
4
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, TypeAlias
5
5
 
6
6
  from pydantic import Field, PrivateAttr, model_validator
7
7
 
@@ -24,10 +24,10 @@ from pymax.types.domain import (
24
24
 
25
25
  from .base import CamelModel
26
26
  from .element import Element
27
- from .enums import MessageStatus
27
+ from .enums import AccessType, LinkType, MessageStatus
28
28
 
29
29
  if TYPE_CHECKING:
30
- from pymax.api.messages.service import MessageService
30
+ from pymax.api.messages import DateTimeUnion, MessageService
31
31
 
32
32
 
33
33
  KnownAttachment: TypeAlias = Annotated[
@@ -47,6 +47,7 @@ KnownAttachment: TypeAlias = Annotated[
47
47
  Attachment: TypeAlias = KnownAttachment | UnknownAttachment
48
48
  SendAttachment: TypeAlias = Photo | File | Video | Poll | Voice | VideoNote
49
49
  SendAttachments: TypeAlias = Sequence[SendAttachment] | None
50
+ Link: TypeAlias = Annotated["ReplyLink | ForwardLink", Field(discriminator="type")]
50
51
 
51
52
 
52
53
  class ReactionCounter(CamelModel):
@@ -91,6 +92,66 @@ class ReadState(CamelModel):
91
92
  mark: int
92
93
 
93
94
 
95
+ class DelayedAttributes(CamelModel):
96
+ time_to_fire: int
97
+ notify_sender: bool
98
+ notify_opponents: bool
99
+
100
+
101
+ class ReplyLink(CamelModel):
102
+ """Источник сообщения, отправленного как reply.
103
+
104
+ :ivar message: Сообщение, на которое ответили.
105
+ :vartype message: Message
106
+ :ivar chat_id: ID чата исходного сообщения.
107
+ :vartype chat_id: int
108
+ """
109
+
110
+ type: Literal[LinkType.REPLY] = LinkType.REPLY
111
+ message: Message
112
+ chat_id: int
113
+
114
+
115
+ class ForwardLink(CamelModel):
116
+ """Источник пересланного сообщения.
117
+
118
+ Помимо исходного сообщения содержит доступные в payload-е имя, ссылку,
119
+ тип доступа и иконку исходного чата. Для скрытого источника эти метаданные
120
+ могут быть ``None``.
121
+
122
+ :ivar message: Исходное сообщение.
123
+ :vartype message: Message
124
+ :ivar chat_id: ID исходного чата.
125
+ :vartype chat_id: int
126
+ """
127
+
128
+ type: Literal[LinkType.FORWARD] = LinkType.FORWARD
129
+ message: Message
130
+ chat_id: int
131
+ chat_name: str | None = None
132
+ chat_link: str | None = None
133
+ chat_access_type: AccessType | None = None
134
+ chat_icon_url: str | None = None
135
+
136
+
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
+
94
155
  class Message(CamelModel):
95
156
  """Сообщение Max с методами для действий над ним.
96
157
 
@@ -130,7 +191,8 @@ class Message(CamelModel):
130
191
  :vartype type: str
131
192
  :ivar cid: Клиентский ID сообщения, если он есть в payload-е.
132
193
  :vartype cid: int | None
133
- :ivar attaches: Вложения сообщения.
194
+ :ivar attaches: Вложения сообщения. Неизвестный ``_type`` представлен как
195
+ ``UnknownAttachment`` с сохраненными дополнительными полями.
134
196
  :vartype attaches: list[Attachment]
135
197
  :ivar stats: Дополнительная статистика сообщения от Max.
136
198
  :vartype stats: dict[str, Any] | None
@@ -150,6 +212,12 @@ class Message(CamelModel):
150
212
  :vartype mark: int | None
151
213
  :ivar elements: Форматированные элементы текста сообщения.
152
214
  :vartype elements: list[Element]
215
+ :ivar delayed_attributes: Время срабатывания в миллисекундах и настройки
216
+ уведомлений для отложенного сообщения; ``None`` для обычного.
217
+ :vartype delayed_attributes: DelayedAttributes | None
218
+ :ivar link: Исходное сообщение и чат для reply или forward; ``None`` для
219
+ обычного сообщения без связи.
220
+ :vartype link: ReplyLink | ForwardLink | None
153
221
  """
154
222
 
155
223
  id: int
@@ -169,6 +237,8 @@ class Message(CamelModel):
169
237
  unread: int | None = None
170
238
  mark: int | None = None
171
239
  elements: list[Element] = Field(default_factory=list)
240
+ delayed_attributes: DelayedAttributes | None = None
241
+ link: Link | None = None
172
242
 
173
243
  _actions: MessageService | None = PrivateAttr(default=None)
174
244
 
@@ -189,6 +259,7 @@ class Message(CamelModel):
189
259
  attachments: SendAttachments = None,
190
260
  *,
191
261
  notify: bool = True,
262
+ send_at: DateTimeUnion | None = None,
192
263
  ) -> Message:
193
264
  """Отправляет ответ на это сообщение в тот же чат.
194
265
 
@@ -198,6 +269,9 @@ class Message(CamelModel):
198
269
  :type attachments: SendAttachments
199
270
  :param notify: Отправить ли получателям push-уведомление.
200
271
  :type notify: bool
272
+ :param send_at: Абсолютный ``datetime``, относительный ``timedelta``
273
+ или Unix time в секундах. ``None`` и ``0`` отправляют сразу.
274
+ :type send_at: DateTimeUnion | None
201
275
  :returns: Отправленное сообщение.
202
276
  :rtype: Message
203
277
  :raises RuntimeError: Если сообщение не привязано к сервису или не
@@ -211,6 +285,7 @@ class Message(CamelModel):
211
285
  reply_to=self.id,
212
286
  attachments=attachments,
213
287
  notify=notify,
288
+ send_at=send_at,
214
289
  )
215
290
 
216
291
  async def answer(
@@ -220,6 +295,7 @@ class Message(CamelModel):
220
295
  attachments: SendAttachments = None,
221
296
  *,
222
297
  notify: bool = True,
298
+ send_at: DateTimeUnion | None = None,
223
299
  ) -> Message:
224
300
  """Отправляет сообщение в тот же чат.
225
301
 
@@ -231,6 +307,9 @@ class Message(CamelModel):
231
307
  :type attachments: SendAttachments
232
308
  :param notify: Отправить ли получателям push-уведомление.
233
309
  :type notify: bool
310
+ :param send_at: Абсолютный ``datetime``, относительный ``timedelta``
311
+ или Unix time в секундах. ``None`` и ``0`` отправляют сразу.
312
+ :type send_at: DateTimeUnion | None
234
313
  :returns: Отправленное сообщение.
235
314
  :rtype: Message
236
315
  :raises RuntimeError: Если сообщение не привязано к сервису или не
@@ -244,6 +323,7 @@ class Message(CamelModel):
244
323
  reply_to=reply_to,
245
324
  attachments=attachments,
246
325
  notify=notify,
326
+ send_at=send_at,
247
327
  )
248
328
 
249
329
  async def forward(
@@ -362,7 +442,7 @@ class Message(CamelModel):
362
442
 
363
443
  return await actions.add_reaction(
364
444
  chat_id=chat_id,
365
- message_id=str(self.id), # :C
445
+ message_id=self.id,
366
446
  reaction=reaction,
367
447
  )
368
448
 
@@ -378,7 +458,7 @@ class Message(CamelModel):
378
458
 
379
459
  return await actions.remove_reaction(
380
460
  chat_id=chat_id,
381
- message_id=str(self.id), # :C x2
461
+ message_id=self.id,
382
462
  )
383
463
 
384
464
  async def get_reactions(self) -> dict[str, ReactionInfo] | None:
@@ -394,7 +474,7 @@ class Message(CamelModel):
394
474
 
395
475
  return await actions.get_reactions(
396
476
  chat_id=chat_id,
397
- message_ids=[str(self.id)], # :C x3
477
+ message_ids=[self.id],
398
478
  )
399
479
 
400
480
  def _bound(self) -> tuple[MessageService, int]:
@@ -0,0 +1,2 @@
1
+ from .catalog import VersionCatalog
2
+ from .exceptions import VersionNotFoundError
@@ -0,0 +1,104 @@
1
+ import json
2
+ from importlib import resources
3
+ from typing import Any
4
+
5
+ import aiohttp
6
+
7
+ from pymax.fingerprint.models import ApkBuildFingerprint
8
+
9
+ from .exceptions import VersionAlreadyExistsError, VersionNotFoundError
10
+
11
+
12
+ class VersionCatalog:
13
+ """Каталог fingerprints Android-клиента Max.
14
+
15
+ Встроенные версии загружаются при создании объекта. При ``remote=True``
16
+ вызов :meth:`load` дополнительно получает каталог с
17
+ ``DEFAULT_REMOTE_URL`` и объединяет его со встроенным. Ошибки сети, HTTP и
18
+ валидации удаленного ответа не подавляются.
19
+
20
+ Args:
21
+ remote: Загружать ли удаленные версии при :meth:`load`.
22
+ """
23
+
24
+ RECOMMENDED_APP_VERSION: str = "26.25.0"
25
+ DEFAULT_REMOTE_URL: str = "https://hashes.pymax.org/versions.json"
26
+
27
+ def __init__(self, remote: bool = False) -> None:
28
+ self.path = resources.files("pymax._data") / "apk_fingerprints.json"
29
+ self.remote = remote
30
+ self._data = self._load_fingerprints()
31
+ self.versions = self._convert_data()
32
+
33
+ @staticmethod
34
+ def recommended() -> str:
35
+ """Возвращает версию, используемую ``Client`` по умолчанию."""
36
+ return VersionCatalog.RECOMMENDED_APP_VERSION
37
+
38
+ async def load(self) -> "VersionCatalog":
39
+ """Загружает удаленные версии, если включен ``remote``.
40
+
41
+ Returns:
42
+ Этот же каталог с добавленными удаленными версиями.
43
+ """
44
+ if not self.remote:
45
+ return self
46
+
47
+ versions = await self._fetch()
48
+ self.versions.update(versions)
49
+
50
+ return self
51
+
52
+ def add(self, version: str, fingerprint: ApkBuildFingerprint, override: bool = False) -> None:
53
+ """Добавляет fingerprint версии в текущий каталог.
54
+
55
+ Args:
56
+ version: Строка версии Android-клиента.
57
+ fingerprint: Fingerprint и build number этой версии.
58
+ override: Заменить существующую запись. По умолчанию ``False``.
59
+
60
+ Raises:
61
+ ValueError: Если версия уже существует и ``override=False``.
62
+ """
63
+ if self.versions.get(version) is not None and override is False:
64
+ raise VersionAlreadyExistsError(version)
65
+
66
+ self.versions[version] = fingerprint
67
+
68
+ def resolve(self, version: str) -> ApkBuildFingerprint:
69
+ """Возвращает fingerprint версии.
70
+
71
+ Raises:
72
+ VersionNotFoundError: Если версии нет в каталоге.
73
+ """
74
+ try:
75
+ return self.versions[version]
76
+ except KeyError as e:
77
+ raise VersionNotFoundError(version) from e
78
+
79
+ async def _fetch(self) -> dict[str, ApkBuildFingerprint]:
80
+ async with (
81
+ aiohttp.ClientSession() as session,
82
+ session.get(self.DEFAULT_REMOTE_URL) as response,
83
+ ):
84
+ response.raise_for_status()
85
+ data = await response.json()
86
+
87
+ if not isinstance(data, dict):
88
+ raise ValueError # TODO: msg
89
+
90
+ versions: dict[str, ApkBuildFingerprint] = {}
91
+ for k, v in data.items():
92
+ versions[k] = ApkBuildFingerprint.model_validate(v)
93
+
94
+ return versions
95
+
96
+ def _load_fingerprints(self) -> dict[str, Any]:
97
+ with self.path.open("r", encoding="utf-8") as f:
98
+ return json.load(f)
99
+
100
+ def _convert_data(self) -> dict[str, ApkBuildFingerprint]:
101
+ versions: dict[str, ApkBuildFingerprint] = {}
102
+ for k, v in self._data.items():
103
+ versions[k] = ApkBuildFingerprint.model_validate(v)
104
+ return versions
@@ -0,0 +1,10 @@
1
+ class VersionNotFoundError(ValueError):
2
+ """Выбранная версия приложения отсутствует в каталоге fingerprints."""
3
+
4
+ def __init__(self, version: str) -> None:
5
+ super().__init__(f"Could not find version {version} in registry")
6
+
7
+
8
+ class VersionAlreadyExistsError(ValueError):
9
+ def __init__(self, version: str) -> None:
10
+ super().__init__(f"Version {version} already exists in registry")