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.
- {maxapi_python-2.4.0.dist-info → maxapi_python-2.4.1.dist-info}/METADATA +6 -4
- {maxapi_python-2.4.0.dist-info → maxapi_python-2.4.1.dist-info}/RECORD +47 -42
- {maxapi_python-2.4.0.dist-info → maxapi_python-2.4.1.dist-info}/WHEEL +1 -1
- pymax/__init__.py +4 -2
- pymax/_data/apk_fingerprints.json +102 -17
- pymax/_data/rootca_ssl_rsa2022.crt +33 -0
- pymax/api/auth/service.py +14 -2
- pymax/api/chats/payloads.py +2 -1
- pymax/api/chats/service.py +7 -0
- pymax/api/messages/__init__.py +1 -1
- pymax/api/messages/payloads.py +9 -3
- pymax/api/messages/service.py +106 -12
- pymax/api/uploads/payloads.py +33 -1
- pymax/api/uploads/service.py +77 -62
- pymax/app.py +23 -4
- pymax/auth/__init__.py +2 -0
- pymax/auth/exceptions.py +9 -0
- pymax/auth/providers.py +6 -2
- pymax/auth/sms.py +11 -1
- pymax/base.py +83 -9
- pymax/client.py +39 -10
- pymax/client_web.py +15 -12
- pymax/config.py +63 -21
- pymax/files/base.py +58 -1
- pymax/files/video.py +32 -40
- pymax/files/voice.py +18 -4
- pymax/fingerprint/fingerprint.py +5 -22
- pymax/fingerprint/models.py +1 -0
- pymax/infra/chat.py +9 -5
- pymax/infra/message.py +9 -3
- pymax/infra/protocol.py +5 -1
- 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/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/video.py +3 -3
- pymax/types/domain/enums.py +7 -0
- pymax/types/domain/message.py +87 -7
- 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 → maxapi_python-2.4.1.dist-info}/licenses/LICENSE +0 -0
pymax/types/domain/message.py
CHANGED
|
@@ -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
|
|
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=
|
|
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=
|
|
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=[
|
|
477
|
+
message_ids=[self.id],
|
|
398
478
|
)
|
|
399
479
|
|
|
400
480
|
def _bound(self) -> tuple[MessageService, int]:
|
|
@@ -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")
|
|
File without changes
|