maxapi-python 2.3.1__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.3.1.dist-info → maxapi_python-2.4.1.dist-info}/METADATA +8 -4
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/RECORD +79 -66
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/WHEEL +1 -1
- pymax/__init__.py +9 -2
- pymax/_data/apk_fingerprints.json +767 -0
- pymax/_data/rootca_ssl_rsa2022.crt +33 -0
- pymax/api/auth/enums.py +1 -0
- pymax/api/auth/payloads.py +28 -2
- pymax/api/auth/service.py +100 -3
- pymax/api/chats/__init__.py +1 -0
- pymax/api/chats/enums.py +15 -0
- pymax/api/chats/payloads.py +18 -2
- pymax/api/chats/service.py +43 -1
- pymax/api/messages/__init__.py +1 -1
- pymax/api/messages/payloads.py +21 -7
- pymax/api/messages/service.py +161 -24
- pymax/api/self/__init__.py +2 -1
- pymax/api/self/enums.py +9 -0
- pymax/api/self/payloads.py +40 -1
- pymax/api/self/service.py +37 -0
- pymax/api/session/service.py +21 -8
- pymax/api/uploads/payloads.py +39 -1
- pymax/api/uploads/service.py +247 -55
- pymax/app.py +91 -18
- 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 +90 -8
- pymax/client.py +39 -10
- pymax/client_web.py +17 -14
- pymax/config.py +98 -19
- pymax/connection/connection.py +29 -18
- pymax/dispatch/enums.py +1 -0
- pymax/dispatch/mapping.py +3 -1
- pymax/dispatch/resolvers.py +7 -1
- pymax/files/__init__.py +4 -1
- pymax/files/base.py +58 -1
- pymax/files/video.py +51 -1
- pymax/files/voice.py +53 -0
- pymax/fingerprint/__init__.py +1 -0
- pymax/fingerprint/fingerprint.py +30 -0
- pymax/fingerprint/models.py +12 -0
- pymax/infra/auth.py +4 -0
- pymax/infra/bots.py +3 -3
- pymax/infra/chat.py +47 -5
- pymax/infra/message.py +49 -14
- pymax/infra/protocol.py +5 -1
- pymax/infra/self.py +25 -1
- pymax/protocol/enums.py +13 -0
- pymax/protocol/tcp/payload.py +21 -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 +17 -4
- pymax/transport/websocket.py +6 -3
- pymax/types/domain/__init__.py +2 -1
- pymax/types/domain/attachments/__init__.py +2 -1
- 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 +33 -1
- pymax/types/domain/attachments/poll.py +111 -0
- pymax/types/domain/attachments/sticker.py +1 -1
- pymax/types/domain/attachments/video.py +32 -11
- pymax/types/domain/chat.py +9 -9
- pymax/types/domain/enums.py +7 -0
- pymax/types/domain/handshake.py +16 -0
- pymax/types/domain/login.py +30 -1
- pymax/types/domain/message.py +108 -27
- pymax/types/domain/user.py +1 -3
- pymax/types/events/__init__.py +1 -0
- pymax/types/events/reaction.py +2 -2
- pymax/types/events/voice.py +5 -0
- pymax/versions/__init__.py +2 -0
- pymax/versions/catalog.py +104 -0
- pymax/versions/exceptions.py +10 -0
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/licenses/LICENSE +0 -0
pymax/protocol/tcp/payload.py
CHANGED
|
@@ -11,6 +11,8 @@ logger = get_logger(__name__)
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class MsgpackPayloadCodec:
|
|
14
|
+
WRAPPED_VALUE_EXT_CODE = 1
|
|
15
|
+
|
|
14
16
|
def _to_msgpack_value(self, value: Any) -> Any:
|
|
15
17
|
if isinstance(value, Enum):
|
|
16
18
|
return value.value
|
|
@@ -30,10 +32,25 @@ class MsgpackPayloadCodec:
|
|
|
30
32
|
def _unpack_stream(
|
|
31
33
|
self, payload_bytes: bytes, *, raw: bool
|
|
32
34
|
) -> list[Any]: # TODO: deprecate? idk
|
|
33
|
-
unpacker = msgpack.Unpacker(
|
|
35
|
+
unpacker = msgpack.Unpacker(
|
|
36
|
+
raw=raw,
|
|
37
|
+
strict_map_key=False,
|
|
38
|
+
ext_hook=self._decode_ext,
|
|
39
|
+
)
|
|
34
40
|
unpacker.feed(payload_bytes)
|
|
35
41
|
return list(unpacker)
|
|
36
42
|
|
|
43
|
+
def _decode_ext(self, code: int, data: bytes) -> Any:
|
|
44
|
+
if code != self.WRAPPED_VALUE_EXT_CODE:
|
|
45
|
+
return msgpack.ExtType(code, data)
|
|
46
|
+
|
|
47
|
+
return msgpack.unpackb(
|
|
48
|
+
data,
|
|
49
|
+
raw=False,
|
|
50
|
+
strict_map_key=False,
|
|
51
|
+
ext_hook=self._decode_ext,
|
|
52
|
+
)
|
|
53
|
+
|
|
37
54
|
def decode(self, payload_bytes: bytes) -> Any:
|
|
38
55
|
if not payload_bytes:
|
|
39
56
|
return {}
|
|
@@ -43,6 +60,7 @@ class MsgpackPayloadCodec:
|
|
|
43
60
|
payload_bytes,
|
|
44
61
|
raw=False,
|
|
45
62
|
strict_map_key=False,
|
|
63
|
+
ext_hook=self._decode_ext,
|
|
46
64
|
)
|
|
47
65
|
|
|
48
66
|
except msgpack.exceptions.ExtraData as e:
|
|
@@ -81,6 +99,8 @@ class TcpPayloadDecoder:
|
|
|
81
99
|
return {self._normalize_key(k): self._normalize_keys(v) for k, v in obj.items()}
|
|
82
100
|
if isinstance(obj, list):
|
|
83
101
|
return [self._normalize_keys(item) for item in obj]
|
|
102
|
+
if isinstance(obj, msgpack.ExtType):
|
|
103
|
+
return obj
|
|
84
104
|
if isinstance(obj, tuple):
|
|
85
105
|
return tuple(self._normalize_keys(item) for item in obj)
|
|
86
106
|
return obj
|
pymax/session/__init__.py
CHANGED
pymax/session/models.py
CHANGED
|
@@ -1,11 +1,41 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field
|
|
2
2
|
|
|
3
|
+
from pymax.api.session.payloads import MobileUserAgentPayload
|
|
3
4
|
from pymax.types.domain.sync import SyncState
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class SessionInfo(BaseModel):
|
|
8
|
+
"""Состояние авторизованной сессии, сохраняемое через ``StoreProtocol``.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
token: Login token Max.
|
|
12
|
+
device_id: ID устройства, связанный с сессией.
|
|
13
|
+
phone: Номер телефона сессии; для web-сессии может быть пустой строкой.
|
|
14
|
+
mt_instance_id: Instance ID устройства. Пустая строка поддерживается для
|
|
15
|
+
сессий, созданных предыдущими версиями PyMax.
|
|
16
|
+
user_agent: Device/user-agent сессии. ``None`` поддерживается для старых
|
|
17
|
+
или пользовательских store, которые его не сохраняют.
|
|
18
|
+
sync: Последние sync-маркеры login.
|
|
19
|
+
"""
|
|
20
|
+
|
|
7
21
|
token: str
|
|
8
22
|
device_id: str
|
|
9
23
|
phone: str
|
|
10
24
|
mt_instance_id: str = ""
|
|
25
|
+
user_agent: MobileUserAgentPayload | None = None
|
|
11
26
|
sync: SyncState = Field(default_factory=SyncState)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_session_user_agent(
|
|
30
|
+
current: MobileUserAgentPayload,
|
|
31
|
+
stored: MobileUserAgentPayload | None,
|
|
32
|
+
) -> MobileUserAgentPayload:
|
|
33
|
+
if stored is None or stored.device_type != current.device_type:
|
|
34
|
+
return current
|
|
35
|
+
|
|
36
|
+
return stored.model_copy(
|
|
37
|
+
update={
|
|
38
|
+
"app_version": current.app_version,
|
|
39
|
+
"build_number": current.build_number,
|
|
40
|
+
}
|
|
41
|
+
)
|
pymax/session/protocol.py
CHANGED
|
@@ -5,10 +5,36 @@ from .models import SessionInfo
|
|
|
5
5
|
|
|
6
6
|
@runtime_checkable
|
|
7
7
|
class StoreProtocol(Protocol):
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
async def
|
|
8
|
+
"""Асинхронный контракт пользовательского хранилища сессии.
|
|
9
|
+
|
|
10
|
+
PyMax использует переданный ``ExtraConfig.store`` только при
|
|
11
|
+
``persist_session=True`` и вызывает :meth:`close` при закрытии runtime.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
async def save_session(self, session_info: SessionInfo) -> None:
|
|
15
|
+
"""Сохраняет или полностью заменяет данные сессии."""
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
async def update_token(self, old_token: str, new_token: str, /) -> None:
|
|
19
|
+
"""Заменяет ``old_token`` у соответствующей сессии."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
async def load_session(self) -> SessionInfo | None:
|
|
23
|
+
"""Возвращает сессию для запуска клиента или ``None``."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
async def load_session_by_device_id(self, device_id: str) -> SessionInfo | None:
|
|
27
|
+
"""Ищет сессию по device ID."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
async def load_session_by_phone(self, phone: str) -> SessionInfo | None:
|
|
31
|
+
"""Ищет сессию по номеру телефона."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
async def delete_session(self, token: str, /) -> None:
|
|
35
|
+
"""Удаляет сессию с переданным token, если она существует."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
async def close(self) -> None:
|
|
39
|
+
"""Освобождает ресурсы хранилища при закрытии runtime."""
|
|
40
|
+
...
|
pymax/session/store.py
CHANGED
|
@@ -2,10 +2,12 @@ from pathlib import Path
|
|
|
2
2
|
|
|
3
3
|
import aiosqlite
|
|
4
4
|
|
|
5
|
+
from pymax.api.session.payloads import MobileUserAgentPayload
|
|
5
6
|
from pymax.logging import get_logger
|
|
6
7
|
from pymax.types.domain.sync import DEFAULT_CONFIG_HASH, SyncState
|
|
7
8
|
|
|
8
9
|
from .models import SessionInfo
|
|
10
|
+
from .protocol import StoreProtocol
|
|
9
11
|
|
|
10
12
|
logger = get_logger(__name__)
|
|
11
13
|
|
|
@@ -18,11 +20,64 @@ SESSION_COLUMNS = """
|
|
|
18
20
|
contacts_sync,
|
|
19
21
|
drafts_sync,
|
|
20
22
|
presence_sync,
|
|
21
|
-
config_hash
|
|
23
|
+
config_hash,
|
|
24
|
+
user_agent
|
|
22
25
|
"""
|
|
23
26
|
|
|
24
27
|
|
|
25
|
-
class
|
|
28
|
+
class InMemoryStore(StoreProtocol):
|
|
29
|
+
"""Хранилище одной сессии в памяти текущего runtime.
|
|
30
|
+
|
|
31
|
+
Данные не записываются на диск и становятся недоступны после пересоздания
|
|
32
|
+
``App``. Обычно этот store выбирается автоматически при
|
|
33
|
+
``ExtraConfig(persist_session=False)``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
) -> None:
|
|
39
|
+
self._session: SessionInfo | None = None
|
|
40
|
+
|
|
41
|
+
async def save_session(self, session_info: SessionInfo) -> None:
|
|
42
|
+
self._session = session_info
|
|
43
|
+
|
|
44
|
+
async def update_token(self, _: str, new_token: str) -> None:
|
|
45
|
+
if not self._session:
|
|
46
|
+
return
|
|
47
|
+
self._session.token = new_token
|
|
48
|
+
|
|
49
|
+
async def load_session(self) -> SessionInfo | None:
|
|
50
|
+
return self._session
|
|
51
|
+
|
|
52
|
+
async def load_session_by_device_id(self, device_id: str) -> SessionInfo | None:
|
|
53
|
+
if self._session and self._session.device_id != device_id:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
return self._session
|
|
57
|
+
|
|
58
|
+
async def load_session_by_phone(self, phone: str) -> SessionInfo | None:
|
|
59
|
+
if self._session and self._session.phone != phone:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
return self._session
|
|
63
|
+
|
|
64
|
+
async def delete_session(self, _: str) -> None:
|
|
65
|
+
self._session = None
|
|
66
|
+
|
|
67
|
+
async def close(self) -> None:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class SessionStore(StoreProtocol):
|
|
72
|
+
"""SQLite-хранилище сессий, используемое по умолчанию.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
work_dir: Директория файла базы данных; создается автоматически.
|
|
76
|
+
db_name: Имя файла базы данных. По умолчанию ``session.db``.
|
|
77
|
+
|
|
78
|
+
Схема существующего файла автоматически дополняется недостающими колонками.
|
|
79
|
+
"""
|
|
80
|
+
|
|
26
81
|
def __init__(self, work_dir: str, db_name: str = "session.db") -> None:
|
|
27
82
|
self.work_dir = Path(work_dir)
|
|
28
83
|
self.work_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -51,7 +106,8 @@ class SessionStore:
|
|
|
51
106
|
contacts_sync INTEGER NOT NULL DEFAULT -1,
|
|
52
107
|
drafts_sync INTEGER NOT NULL DEFAULT -1,
|
|
53
108
|
presence_sync INTEGER NOT NULL DEFAULT -1,
|
|
54
|
-
config_hash TEXT NOT NULL DEFAULT ''
|
|
109
|
+
config_hash TEXT NOT NULL DEFAULT '',
|
|
110
|
+
user_agent TEXT
|
|
55
111
|
)
|
|
56
112
|
"""
|
|
57
113
|
)
|
|
@@ -61,6 +117,7 @@ class SessionStore:
|
|
|
61
117
|
await self._ensure_column(conn, "drafts_sync", "INTEGER NOT NULL DEFAULT -1")
|
|
62
118
|
await self._ensure_column(conn, "presence_sync", "INTEGER NOT NULL DEFAULT -1")
|
|
63
119
|
await self._ensure_column(conn, "config_hash", "TEXT NOT NULL DEFAULT ''")
|
|
120
|
+
await self._ensure_column(conn, "user_agent", "TEXT")
|
|
64
121
|
await conn.execute(
|
|
65
122
|
"""
|
|
66
123
|
UPDATE sessions
|
|
@@ -102,9 +159,10 @@ class SessionStore:
|
|
|
102
159
|
contacts_sync,
|
|
103
160
|
drafts_sync,
|
|
104
161
|
presence_sync,
|
|
105
|
-
config_hash
|
|
162
|
+
config_hash,
|
|
163
|
+
user_agent
|
|
106
164
|
)
|
|
107
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
165
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
108
166
|
""",
|
|
109
167
|
(
|
|
110
168
|
session_info.token,
|
|
@@ -116,6 +174,11 @@ class SessionStore:
|
|
|
116
174
|
session_info.sync.drafts_sync,
|
|
117
175
|
session_info.sync.presence_sync,
|
|
118
176
|
session_info.sync.config_hash,
|
|
177
|
+
(
|
|
178
|
+
session_info.user_agent.model_dump_json(by_alias=True, exclude_none=True)
|
|
179
|
+
if session_info.user_agent is not None
|
|
180
|
+
else None
|
|
181
|
+
),
|
|
119
182
|
),
|
|
120
183
|
)
|
|
121
184
|
await conn.commit()
|
|
@@ -233,6 +296,11 @@ class SessionStore:
|
|
|
233
296
|
device_id=row["device_id"],
|
|
234
297
|
phone=row["phone"],
|
|
235
298
|
mt_instance_id=row["mt_instance_id"] or "",
|
|
299
|
+
user_agent=(
|
|
300
|
+
MobileUserAgentPayload.model_validate_json(row["user_agent"])
|
|
301
|
+
if row["user_agent"] is not None
|
|
302
|
+
else None
|
|
303
|
+
),
|
|
236
304
|
sync=SyncState(
|
|
237
305
|
chats_sync=row["chats_sync"],
|
|
238
306
|
contacts_sync=row["contacts_sync"],
|
pymax/transport/tcp.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import ssl
|
|
3
|
+
from importlib import resources
|
|
2
4
|
|
|
3
5
|
from python_socks.async_.asyncio import Proxy
|
|
4
6
|
|
|
@@ -7,6 +9,7 @@ from pymax.logging import get_logger
|
|
|
7
9
|
from .base import Transport
|
|
8
10
|
|
|
9
11
|
logger = get_logger(__name__)
|
|
12
|
+
_CLOSE_TIMEOUT = 5.0
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
class TCPTransport(Transport):
|
|
@@ -15,6 +18,10 @@ class TCPTransport(Transport):
|
|
|
15
18
|
self._port = port
|
|
16
19
|
self._proxy = proxy
|
|
17
20
|
self._use_ssl = use_ssl
|
|
21
|
+
self._ssl_ctx = ssl.create_default_context()
|
|
22
|
+
self._ssl_ctx.load_verify_locations(
|
|
23
|
+
str(resources.files("pymax._data") / "rootca_ssl_rsa2022.crt")
|
|
24
|
+
)
|
|
18
25
|
self._reader = None
|
|
19
26
|
self._writer = None
|
|
20
27
|
|
|
@@ -25,6 +32,9 @@ class TCPTransport(Transport):
|
|
|
25
32
|
self._port,
|
|
26
33
|
self._use_ssl,
|
|
27
34
|
)
|
|
35
|
+
ssl_ctx = self._ssl_ctx if self._use_ssl else None
|
|
36
|
+
server_hostname = self._host if self._use_ssl else None
|
|
37
|
+
|
|
28
38
|
if self._proxy:
|
|
29
39
|
logger.debug("tcp connecting via proxy %s", self._proxy)
|
|
30
40
|
proxy = Proxy.from_url(self._proxy)
|
|
@@ -32,10 +42,9 @@ class TCPTransport(Transport):
|
|
|
32
42
|
dest_host=self._host,
|
|
33
43
|
dest_port=self._port,
|
|
34
44
|
)
|
|
35
|
-
server_hostname = self._host if self._use_ssl else None
|
|
36
45
|
self._reader, self._writer = await asyncio.open_connection(
|
|
37
46
|
sock=sock,
|
|
38
|
-
ssl=
|
|
47
|
+
ssl=ssl_ctx,
|
|
39
48
|
server_hostname=server_hostname,
|
|
40
49
|
)
|
|
41
50
|
logger.info(
|
|
@@ -49,7 +58,7 @@ class TCPTransport(Transport):
|
|
|
49
58
|
self._reader, self._writer = await asyncio.open_connection(
|
|
50
59
|
self._host,
|
|
51
60
|
self._port,
|
|
52
|
-
ssl=
|
|
61
|
+
ssl=ssl_ctx,
|
|
53
62
|
)
|
|
54
63
|
logger.info(
|
|
55
64
|
"tcp connected host=%s port=%s ssl=%s",
|
|
@@ -66,7 +75,11 @@ class TCPTransport(Transport):
|
|
|
66
75
|
if writer:
|
|
67
76
|
logger.debug("tcp close")
|
|
68
77
|
writer.close()
|
|
69
|
-
|
|
78
|
+
try:
|
|
79
|
+
await asyncio.wait_for(writer.wait_closed(), _CLOSE_TIMEOUT)
|
|
80
|
+
except (OSError, TimeoutError) as e:
|
|
81
|
+
logger.warning("tcp close did not finish cleanly: %s", e)
|
|
82
|
+
writer.transport.abort()
|
|
70
83
|
logger.debug("tcp closed")
|
|
71
84
|
|
|
72
85
|
async def send(self, data: bytes | str) -> None:
|
pymax/transport/websocket.py
CHANGED
|
@@ -17,11 +17,14 @@ class WebSocketTransport(Transport):
|
|
|
17
17
|
async def connect(self) -> None:
|
|
18
18
|
if self.proxy:
|
|
19
19
|
self.ws = await client.connect(
|
|
20
|
-
self.url,
|
|
20
|
+
self.url,
|
|
21
|
+
origin=Origin("https://web.max.ru"),
|
|
22
|
+
proxy=self.proxy,
|
|
23
|
+
max_size=1024 * 1024 * 10, # 10 MB
|
|
21
24
|
)
|
|
22
25
|
else:
|
|
23
26
|
self.ws = await client.connect(
|
|
24
|
-
self.url, origin=Origin("https://web.max.ru")
|
|
27
|
+
self.url, origin=Origin("https://web.max.ru"), max_size=1024 * 1024 * 10
|
|
25
28
|
) # TODO: origin should be configurable
|
|
26
29
|
|
|
27
30
|
async def close(self) -> None:
|
|
@@ -41,7 +44,7 @@ class WebSocketTransport(Transport):
|
|
|
41
44
|
if self.ws is None or not self.connected:
|
|
42
45
|
raise ConnectionError("Not connected to the server")
|
|
43
46
|
|
|
44
|
-
return await self.ws.recv(decode=
|
|
47
|
+
return await self.ws.recv(decode=False)
|
|
45
48
|
|
|
46
49
|
@property
|
|
47
50
|
def connected(self) -> bool:
|
pymax/types/domain/__init__.py
CHANGED
|
@@ -3,7 +3,8 @@ from .bots import InitData
|
|
|
3
3
|
from .chat import Chat
|
|
4
4
|
from .error import MaxApiError
|
|
5
5
|
from .folder import Folder, FolderList, FolderUpdate
|
|
6
|
-
from .
|
|
6
|
+
from .handshake import HandshakeResponse
|
|
7
|
+
from .login import Login2Flags, Login2Response, LoginResponse
|
|
7
8
|
from .member import Member
|
|
8
9
|
from .message import Message, ReactionCounter, ReactionInfo, ReadState
|
|
9
10
|
from .name import Name
|
|
@@ -2,10 +2,11 @@ from .audio import AudioAttachment
|
|
|
2
2
|
from .call import CallAttachment
|
|
3
3
|
from .contact import ContactAttachment
|
|
4
4
|
from .control import ControlAttachment
|
|
5
|
-
from .enums import AttachmentType
|
|
5
|
+
from .enums import AttachmentType, PollFlags
|
|
6
6
|
from .file import FileAttachment, FileRequest
|
|
7
7
|
from .keyboards import InlineKeyboardAttachment
|
|
8
8
|
from .photo import PhotoAttachment
|
|
9
|
+
from .poll import Poll, PollAnswer, PollAttachment, PollState
|
|
9
10
|
from .share import ShareAttachment
|
|
10
11
|
from .sticker import StickerAttachment
|
|
11
12
|
from .unknown import UnknownAttachment
|
|
@@ -4,7 +4,7 @@ from pydantic import Field
|
|
|
4
4
|
|
|
5
5
|
from pymax.types.domain.base import CamelModel
|
|
6
6
|
|
|
7
|
-
from .enums import AttachmentType
|
|
7
|
+
from .enums import AttachmentType, CallType, HangupType
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class CallAttachment(CamelModel):
|
|
@@ -18,9 +18,15 @@ class CallAttachment(CamelModel):
|
|
|
18
18
|
:vartype conversation_id: str | int | None
|
|
19
19
|
:ivar contact_ids: ID участников звонка.
|
|
20
20
|
:vartype contact_ids: list[int]
|
|
21
|
+
:ivar call_type: Тип звонка, если Max его прислал.
|
|
22
|
+
:vartype call_type: CallType | None
|
|
23
|
+
:ivar hangup_type: Причина завершения звонка, если она есть в payload-е.
|
|
24
|
+
:vartype hangup_type: HangupType | None
|
|
21
25
|
"""
|
|
22
26
|
|
|
23
27
|
type: Literal[AttachmentType.CALL] = Field(alias="_type")
|
|
24
28
|
duration: int | None = None
|
|
25
29
|
conversation_id: str | int | None = None
|
|
26
30
|
contact_ids: list[int] = Field(default_factory=list)
|
|
31
|
+
call_type: CallType | None = None
|
|
32
|
+
hangup_type: HangupType | None = None
|
|
@@ -10,8 +10,8 @@ from .enums import AttachmentType
|
|
|
10
10
|
class ContactAttachment(CamelModel):
|
|
11
11
|
"""Контактное вложение сообщения.
|
|
12
12
|
|
|
13
|
-
:ivar contact_id: ID
|
|
14
|
-
:vartype contact_id: int
|
|
13
|
+
:ivar contact_id: ID контакта, если Max его прислал.
|
|
14
|
+
:vartype contact_id: int | None
|
|
15
15
|
:ivar first_name: Имя контакта.
|
|
16
16
|
:vartype first_name: str | None
|
|
17
17
|
:ivar last_name: Фамилия контакта.
|
|
@@ -24,7 +24,7 @@ class ContactAttachment(CamelModel):
|
|
|
24
24
|
:vartype type: Literal[AttachmentType.CONTACT]
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
-
contact_id: int
|
|
27
|
+
contact_id: int | None = None
|
|
28
28
|
first_name: str | None = None
|
|
29
29
|
last_name: str | None = None
|
|
30
30
|
name: str | None = None
|
|
@@ -14,7 +14,10 @@ class ControlAttachment(CamelModel):
|
|
|
14
14
|
:vartype type: Literal[AttachmentType.CONTROL]
|
|
15
15
|
:ivar event: Событие управления.
|
|
16
16
|
:vartype event: str
|
|
17
|
+
:ivar title: Заголовок служебного события, если он есть в payload-е.
|
|
18
|
+
:vartype title: str | None
|
|
17
19
|
"""
|
|
18
20
|
|
|
19
21
|
type: Literal[AttachmentType.CONTROL] = Field(alias="_type")
|
|
20
22
|
event: str
|
|
23
|
+
title: str | None = None
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from enum import Enum
|
|
1
|
+
from enum import Enum, IntFlag
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class AttachmentType(str, Enum):
|
|
@@ -14,6 +14,7 @@ class AttachmentType(str, Enum):
|
|
|
14
14
|
CALL = "CALL"
|
|
15
15
|
SHARE = "SHARE"
|
|
16
16
|
INLINE_KEYBOARD = "INLINE_KEYBOARD"
|
|
17
|
+
POLL = "POLL"
|
|
17
18
|
UNKNOWN = "UNKNOWN"
|
|
18
19
|
|
|
19
20
|
|
|
@@ -26,3 +27,34 @@ class TranscriptionStatus(str, Enum):
|
|
|
26
27
|
PROCESSING = "PROCESSING"
|
|
27
28
|
SUCCESS = "SUCCESS"
|
|
28
29
|
UNKNOWN = "UNKNOWN"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PollFlags(IntFlag):
|
|
33
|
+
"""Настройки опроса, представленные битовой маской.
|
|
34
|
+
|
|
35
|
+
Несколько настроек объединяются оператором ``|``. Числовые значения,
|
|
36
|
+
полученные от Max, автоматически преобразуются в ``PollFlags``.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
FLAG_SETTINGS_ANONYMOUS = 1
|
|
40
|
+
FLAG_SETTINGS_MULTISELECT = 2
|
|
41
|
+
FLAG_SETTINGS_REVOTE = 4
|
|
42
|
+
FLAG_SETTINGS_CLOSED = 8
|
|
43
|
+
FLAG_SETTINGS_QUIZ = 16
|
|
44
|
+
FLAG_SETTINGS_CAN_FORWARD = 32
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CallType(str, Enum):
|
|
48
|
+
"""Тип звонка во входящем ``CallAttachment``."""
|
|
49
|
+
|
|
50
|
+
AUDIO = "AUDIO"
|
|
51
|
+
VIDEO = "VIDEO"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class HangupType(str, Enum):
|
|
55
|
+
"""Причина завершения звонка во входящем ``CallAttachment``."""
|
|
56
|
+
|
|
57
|
+
MISSED = "MISSED"
|
|
58
|
+
REJECTED = "REJECTED"
|
|
59
|
+
CANCELED = "CANCELED"
|
|
60
|
+
HUNGUP = "HUNGUP"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from pymax.types.domain.base import CamelModel
|
|
6
|
+
|
|
7
|
+
from .enums import AttachmentType, PollFlags
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PollAnswer(CamelModel):
|
|
11
|
+
"""Вариант ответа в опросе.
|
|
12
|
+
|
|
13
|
+
:ivar text: Текст варианта ответа.
|
|
14
|
+
:vartype text: str
|
|
15
|
+
:ivar answer_id: ID варианта, назначенный Max.
|
|
16
|
+
:vartype answer_id: int | None
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
text: str
|
|
20
|
+
answer_id: int | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PollVote(CamelModel):
|
|
24
|
+
"""Голос пользователя за один вариант ответа.
|
|
25
|
+
|
|
26
|
+
:ivar timestamp: Время голосования в формате Unix time.
|
|
27
|
+
:vartype timestamp: int
|
|
28
|
+
:ivar user_id: ID проголосовавшего пользователя.
|
|
29
|
+
:vartype user_id: int
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
timestamp: int
|
|
33
|
+
user_id: int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PollResult(CamelModel):
|
|
37
|
+
"""Результат голосования по одному варианту ответа.
|
|
38
|
+
|
|
39
|
+
:ivar answer_id: ID варианта ответа.
|
|
40
|
+
:vartype answer_id: int
|
|
41
|
+
:ivar vote_count: Количество голосов.
|
|
42
|
+
:vartype vote_count: int
|
|
43
|
+
:ivar votes: Голоса пользователей, доступные текущему аккаунту.
|
|
44
|
+
:vartype votes: list[PollVote]
|
|
45
|
+
:ivar rate: Доля голосов в формате, возвращаемом Max.
|
|
46
|
+
:vartype rate: int
|
|
47
|
+
:ivar options: Дополнительные параметры результата от Max.
|
|
48
|
+
:vartype options: int
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
answer_id: int
|
|
52
|
+
vote_count: int
|
|
53
|
+
votes: list[PollVote]
|
|
54
|
+
rate: int
|
|
55
|
+
options: int
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PollState(CamelModel):
|
|
59
|
+
"""Текущее состояние голосования.
|
|
60
|
+
|
|
61
|
+
:ivar total: Общее количество голосов.
|
|
62
|
+
:vartype total: int
|
|
63
|
+
:ivar result: Результаты по вариантам ответа.
|
|
64
|
+
:vartype result: list[PollResult] | None
|
|
65
|
+
:ivar voter_preview_ids: ID пользователей для предпросмотра списка
|
|
66
|
+
проголосовавших.
|
|
67
|
+
:vartype voter_preview_ids: list[int]
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
total: int = 0
|
|
71
|
+
result: list[PollResult] | None = None
|
|
72
|
+
voter_preview_ids: list[int]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Poll(CamelModel):
|
|
76
|
+
"""Опрос для отправки в сообщении.
|
|
77
|
+
|
|
78
|
+
:ivar title: Вопрос или заголовок опроса.
|
|
79
|
+
:vartype title: str
|
|
80
|
+
:ivar answers: Варианты ответа.
|
|
81
|
+
:vartype answers: list[PollAnswer]
|
|
82
|
+
:ivar settings: Настройки опроса. Несколько ``PollFlags`` объединяются
|
|
83
|
+
оператором ``|``.
|
|
84
|
+
:vartype settings: PollFlags
|
|
85
|
+
:ivar type: Тип вложения.
|
|
86
|
+
:vartype type: Literal[AttachmentType.POLL]
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
title: str
|
|
90
|
+
answers: list[PollAnswer]
|
|
91
|
+
settings: PollFlags
|
|
92
|
+
type: Literal[AttachmentType.POLL] = Field(alias="_type", default=AttachmentType.POLL)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PollAttachment(Poll):
|
|
96
|
+
"""Опрос, полученный как вложение сообщения.
|
|
97
|
+
|
|
98
|
+
Помимо параметров опроса содержит назначенный Max ID, версию и текущее
|
|
99
|
+
состояние голосования.
|
|
100
|
+
|
|
101
|
+
:ivar poll_id: ID опроса.
|
|
102
|
+
:vartype poll_id: int
|
|
103
|
+
:ivar version: Версия данных опроса.
|
|
104
|
+
:vartype version: int
|
|
105
|
+
:ivar state: Текущее состояние голосования.
|
|
106
|
+
:vartype state: PollState
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
poll_id: int
|
|
110
|
+
version: int
|
|
111
|
+
state: PollState
|