maxapi-python 2.1.2__py3-none-any.whl → 2.2.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.
- {maxapi_python-2.1.2.dist-info → maxapi_python-2.2.0.dist-info}/METADATA +3 -11
- {maxapi_python-2.1.2.dist-info → maxapi_python-2.2.0.dist-info}/RECORD +66 -60
- pymax/__init__.py +18 -3
- pymax/api/auth/payloads.py +7 -0
- pymax/api/auth/service.py +33 -30
- pymax/api/binding.py +57 -0
- pymax/api/chats/service.py +34 -47
- pymax/api/messages/enums.py +1 -0
- pymax/api/messages/payloads.py +16 -1
- pymax/api/messages/service.py +85 -33
- pymax/api/models.py +4 -6
- pymax/api/response.py +2 -2
- pymax/api/self/service.py +17 -26
- pymax/api/session/payloads.py +2 -9
- pymax/api/session/service.py +1 -3
- pymax/api/uploads/payloads.py +3 -9
- pymax/api/uploads/service.py +33 -99
- pymax/api/users/service.py +8 -16
- pymax/app.py +20 -4
- pymax/auth/qr.py +3 -9
- pymax/auth/sms.py +23 -11
- pymax/base.py +38 -1
- pymax/client.py +3 -5
- pymax/client_web.py +1 -2
- pymax/config.py +42 -3
- pymax/connection/connection.py +48 -19
- pymax/connection/readers/tcp.py +1 -3
- pymax/dispatch/dispatcher.py +36 -18
- pymax/dispatch/enums.py +4 -0
- pymax/dispatch/mapping.py +34 -11
- pymax/dispatch/resolvers.py +18 -0
- pymax/dispatch/router.py +34 -0
- pymax/files/photo.py +4 -2
- pymax/formatting/markdown.py +22 -13
- pymax/infra/chat.py +12 -0
- pymax/infra/message.py +74 -3
- pymax/logging.py +35 -3
- pymax/protocol/tcp/compression.py +1 -3
- pymax/protocol/tcp/framing.py +1 -3
- pymax/protocol/tcp/payload.py +22 -42
- pymax/protocol/tcp/protocol.py +2 -8
- pymax/protocol/ws/protocol.py +3 -9
- pymax/session/protocol.py +2 -6
- pymax/session/store.py +8 -24
- pymax/telemetry/navigation.py +1 -3
- pymax/telemetry/service.py +5 -17
- pymax/transport/tcp.py +1 -3
- pymax/types/domain/attachments/__init__.py +1 -0
- pymax/types/domain/attachments/audio.py +4 -4
- pymax/types/domain/attachments/enums.py +1 -0
- pymax/types/domain/attachments/unknown.py +35 -0
- pymax/types/domain/attachments/video.py +2 -2
- pymax/types/domain/auth.py +24 -2
- pymax/types/domain/chat.py +38 -1
- pymax/types/domain/element.py +3 -3
- pymax/types/domain/message.py +34 -2
- pymax/types/domain/presence.py +3 -3
- pymax/types/domain/sync.py +5 -21
- pymax/types/events/__init__.py +4 -0
- pymax/types/events/mark.py +23 -0
- pymax/types/events/message.py +57 -5
- pymax/types/events/presence.py +15 -0
- pymax/types/events/reaction.py +21 -0
- pymax/types/events/typing.py +14 -0
- {maxapi_python-2.1.2.dist-info → maxapi_python-2.2.0.dist-info}/WHEEL +0 -0
- {maxapi_python-2.1.2.dist-info → maxapi_python-2.2.0.dist-info}/licenses/LICENSE +0 -0
pymax/logging.py
CHANGED
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
from typing import TextIO
|
|
5
5
|
|
|
6
6
|
DATE_FORMAT = "%H:%M:%S"
|
|
7
|
+
PYMAX_HANDLER_ATTR = "_pymax_pretty_handler"
|
|
7
8
|
|
|
8
9
|
RESET = "\x1b[0m"
|
|
9
10
|
DIM = "\x1b[2m"
|
|
@@ -56,17 +57,22 @@ def configure_logging(
|
|
|
56
57
|
*,
|
|
57
58
|
stream: TextIO | None = None,
|
|
58
59
|
use_colors: bool | None = None,
|
|
60
|
+
force: bool = False,
|
|
59
61
|
) -> None:
|
|
60
62
|
"""Настраивает pretty-логи для logger-а ``pymax``.
|
|
61
63
|
|
|
62
64
|
Обычно уровень логов задают через ``ExtraConfig(log_level="DEBUG")``.
|
|
63
|
-
|
|
65
|
+
PyMax ставит свой handler только если приложение еще не настроило logging.
|
|
66
|
+
Вызывайте эту функцию с ``force=True``, если хотите принудительно включить
|
|
67
|
+
pretty-логи PyMax.
|
|
64
68
|
|
|
65
69
|
Args:
|
|
66
70
|
level: Уровень логирования: строка вроде ``"DEBUG"`` или число из
|
|
67
71
|
модуля ``logging``.
|
|
68
72
|
stream: Поток для вывода. По умолчанию ``sys.stderr``.
|
|
69
73
|
use_colors: Включить ANSI-цвета. Если ``None``, определяется по TTY.
|
|
74
|
+
force: Заменить существующие handler-ы logger-а ``pymax`` на pretty
|
|
75
|
+
handler PyMax.
|
|
70
76
|
|
|
71
77
|
Returns:
|
|
72
78
|
``None``.
|
|
@@ -79,22 +85,32 @@ def configure_logging(
|
|
|
79
85
|
configure_logging("DEBUG", use_colors=False)
|
|
80
86
|
"""
|
|
81
87
|
stream = stream or sys.stderr
|
|
88
|
+
if stream is None:
|
|
89
|
+
raise RuntimeError("No logging stream is available")
|
|
82
90
|
|
|
83
91
|
if use_colors is None:
|
|
84
92
|
use_colors = hasattr(stream, "isatty") and stream.isatty()
|
|
85
93
|
|
|
86
94
|
logger = logging.getLogger("pymax")
|
|
95
|
+
level_value = _normalize_level(level)
|
|
96
|
+
logger.setLevel(level_value)
|
|
97
|
+
|
|
98
|
+
if not force and _logging_already_configured(logger):
|
|
99
|
+
if logging.getLogger().handlers and not _has_non_null_handlers(logger):
|
|
100
|
+
logger.propagate = True
|
|
101
|
+
return
|
|
102
|
+
|
|
87
103
|
logger.handlers.clear()
|
|
88
|
-
logger.setLevel(_normalize_level(level))
|
|
89
104
|
logger.propagate = False
|
|
90
105
|
|
|
91
106
|
handler = logging.StreamHandler(stream)
|
|
92
|
-
handler.setLevel(
|
|
107
|
+
handler.setLevel(level_value)
|
|
93
108
|
handler.setFormatter(
|
|
94
109
|
PrettyFormatter(
|
|
95
110
|
use_colors=use_colors,
|
|
96
111
|
)
|
|
97
112
|
)
|
|
113
|
+
setattr(handler, PYMAX_HANDLER_ATTR, True)
|
|
98
114
|
|
|
99
115
|
logger.addHandler(handler)
|
|
100
116
|
|
|
@@ -126,4 +142,20 @@ def _strip_ansi(text: str) -> str:
|
|
|
126
142
|
return re.sub(r"\x1b\[[0-9;]*m", "", text)
|
|
127
143
|
|
|
128
144
|
|
|
145
|
+
def _logging_already_configured(logger: logging.Logger) -> bool:
|
|
146
|
+
return bool(logging.getLogger().handlers or _has_external_handlers(logger))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _has_non_null_handlers(logger: logging.Logger) -> bool:
|
|
150
|
+
return any(not isinstance(handler, logging.NullHandler) for handler in logger.handlers)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _has_external_handlers(logger: logging.Logger) -> bool:
|
|
154
|
+
return any(
|
|
155
|
+
not isinstance(handler, logging.NullHandler)
|
|
156
|
+
and not getattr(handler, PYMAX_HANDLER_ATTR, False)
|
|
157
|
+
for handler in logger.handlers
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
129
161
|
logging.getLogger("pymax").addHandler(logging.NullHandler())
|
pymax/protocol/tcp/framing.py
CHANGED
|
@@ -31,9 +31,7 @@ class TcpPacketFramer:
|
|
|
31
31
|
if len(data) < self.HEADER_SIZE:
|
|
32
32
|
return None
|
|
33
33
|
|
|
34
|
-
ver, cmd, seq, opcode, packed_len = self.HEADER_STRUCT.unpack_from(
|
|
35
|
-
data, 0
|
|
36
|
-
)
|
|
34
|
+
ver, cmd, seq, opcode, packed_len = self.HEADER_STRUCT.unpack_from(data, 0)
|
|
37
35
|
flags = (packed_len >> 24) & 0xFF
|
|
38
36
|
payload_len = packed_len & 0x00FFFFFF
|
|
39
37
|
|
pymax/protocol/tcp/payload.py
CHANGED
|
@@ -15,10 +15,7 @@ class MsgpackPayloadCodec:
|
|
|
15
15
|
if isinstance(value, Enum):
|
|
16
16
|
return value.value
|
|
17
17
|
if isinstance(value, dict):
|
|
18
|
-
return {
|
|
19
|
-
self._to_msgpack_value(k): self._to_msgpack_value(v)
|
|
20
|
-
for k, v in value.items()
|
|
21
|
-
}
|
|
18
|
+
return {self._to_msgpack_value(k): self._to_msgpack_value(v) for k, v in value.items()}
|
|
22
19
|
if isinstance(value, list):
|
|
23
20
|
return [self._to_msgpack_value(item) for item in value]
|
|
24
21
|
if isinstance(value, tuple):
|
|
@@ -28,56 +25,42 @@ class MsgpackPayloadCodec:
|
|
|
28
25
|
def encode(self, payload: object) -> bytes:
|
|
29
26
|
if payload is None:
|
|
30
27
|
return b""
|
|
31
|
-
return (
|
|
32
|
-
msgpack.packb(self._to_msgpack_value(payload), use_bin_type=True)
|
|
33
|
-
or b""
|
|
34
|
-
)
|
|
28
|
+
return msgpack.packb(self._to_msgpack_value(payload), use_bin_type=True) or b""
|
|
35
29
|
|
|
36
|
-
def _unpack_stream(
|
|
30
|
+
def _unpack_stream(
|
|
31
|
+
self, payload_bytes: bytes, *, raw: bool
|
|
32
|
+
) -> list[Any]: # TODO: deprecate? idk
|
|
37
33
|
unpacker = msgpack.Unpacker(raw=raw, strict_map_key=False)
|
|
38
34
|
unpacker.feed(payload_bytes)
|
|
39
35
|
return list(unpacker)
|
|
40
36
|
|
|
41
|
-
def decode(self, payload_bytes: bytes) ->
|
|
37
|
+
def decode(self, payload_bytes: bytes) -> Any:
|
|
42
38
|
if not payload_bytes:
|
|
43
39
|
return {}
|
|
44
40
|
|
|
45
41
|
try:
|
|
46
42
|
return msgpack.unpackb(
|
|
47
|
-
payload_bytes,
|
|
43
|
+
payload_bytes,
|
|
44
|
+
raw=False,
|
|
45
|
+
strict_map_key=False,
|
|
48
46
|
)
|
|
49
|
-
except msgpack.exceptions.ExtraData as e:
|
|
50
|
-
if isinstance(e.unpacked, dict):
|
|
51
|
-
logger.debug(
|
|
52
|
-
"msgpack payload has trailing data unpacked_type=%s extra_bytes=%s extra_head=%s",
|
|
53
|
-
type(e.unpacked).__name__,
|
|
54
|
-
len(e.extra),
|
|
55
|
-
e.extra[:16].hex(),
|
|
56
|
-
)
|
|
57
|
-
return e.unpacked
|
|
58
47
|
|
|
59
|
-
|
|
60
|
-
values = self._unpack_stream(payload_bytes, raw=False)
|
|
61
|
-
except UnicodeDecodeError:
|
|
62
|
-
values = self._unpack_stream(payload_bytes, raw=True)
|
|
48
|
+
except msgpack.exceptions.ExtraData as e:
|
|
63
49
|
logger.debug(
|
|
64
|
-
"msgpack
|
|
65
|
-
|
|
50
|
+
"msgpack extra data: unpacked_type=%s extra_len=%s extra_head=%s payload_head=%s",
|
|
51
|
+
type(e.unpacked).__name__,
|
|
66
52
|
len(e.extra),
|
|
53
|
+
e.extra[:64].hex(),
|
|
54
|
+
payload_bytes[:128].hex(),
|
|
67
55
|
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"msgpack payload decoded with raw bytes objects=%s",
|
|
76
|
-
[type(value).__name__ for value in values],
|
|
56
|
+
return e.unpacked
|
|
57
|
+
|
|
58
|
+
except Exception:
|
|
59
|
+
logger.exception(
|
|
60
|
+
"msgpack decode failed: payload_len=%s payload_head=%s",
|
|
61
|
+
len(payload_bytes),
|
|
62
|
+
payload_bytes[:128].hex(),
|
|
77
63
|
)
|
|
78
|
-
for value in values:
|
|
79
|
-
if isinstance(value, dict):
|
|
80
|
-
return value
|
|
81
64
|
raise
|
|
82
65
|
|
|
83
66
|
|
|
@@ -93,10 +76,7 @@ class TcpPayloadDecoder:
|
|
|
93
76
|
|
|
94
77
|
def _normalize_keys(self, obj: Any) -> Any:
|
|
95
78
|
if isinstance(obj, dict):
|
|
96
|
-
return {
|
|
97
|
-
self._normalize_key(k): self._normalize_keys(v)
|
|
98
|
-
for k, v in obj.items()
|
|
99
|
-
}
|
|
79
|
+
return {self._normalize_key(k): self._normalize_keys(v) for k, v in obj.items()}
|
|
100
80
|
if isinstance(obj, list):
|
|
101
81
|
return [self._normalize_keys(item) for item in obj]
|
|
102
82
|
if isinstance(obj, tuple):
|
pymax/protocol/tcp/protocol.py
CHANGED
|
@@ -25,11 +25,7 @@ class TcpProtocol(BaseProtocol):
|
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
def encode(self, frame: OutboundFrame) -> bytes:
|
|
28
|
-
payload_bytes = (
|
|
29
|
-
self.serializer.encode(frame.payload)
|
|
30
|
-
if frame.payload is not None
|
|
31
|
-
else b""
|
|
32
|
-
)
|
|
28
|
+
payload_bytes = self.serializer.encode(frame.payload) if frame.payload is not None else b""
|
|
33
29
|
|
|
34
30
|
flags = 0
|
|
35
31
|
|
|
@@ -52,9 +48,7 @@ class TcpProtocol(BaseProtocol):
|
|
|
52
48
|
|
|
53
49
|
packed_packet = self.framer.unpack(raw)
|
|
54
50
|
if not packed_packet:
|
|
55
|
-
return InboundFrame(
|
|
56
|
-
opcode=0, cmd=0, seq=None, payload=None, raw=None
|
|
57
|
-
)
|
|
51
|
+
return InboundFrame(opcode=0, cmd=0, seq=None, payload=None, raw=None)
|
|
58
52
|
|
|
59
53
|
logger.debug(
|
|
60
54
|
"tcp frame decoded header ver=%s cmd=%s seq=%s opcode=%s flags=%s payload_len=%s",
|
pymax/protocol/ws/protocol.py
CHANGED
|
@@ -20,14 +20,8 @@ class WsProtocol(BaseProtocol):
|
|
|
20
20
|
data = json.loads(raw)
|
|
21
21
|
return InboundFrame.model_validate(data)
|
|
22
22
|
except json.JSONDecodeError:
|
|
23
|
-
logger.debug(
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
return InboundFrame(
|
|
27
|
-
opcode=0, cmd=0, seq=None, payload=None, raw=None
|
|
28
|
-
)
|
|
23
|
+
logger.debug("failed to decode websocket frame json", exc_info=True)
|
|
24
|
+
return InboundFrame(opcode=0, cmd=0, seq=None, payload=None, raw=None)
|
|
29
25
|
except ValidationError:
|
|
30
26
|
logger.debug("failed to validate websocket frame", exc_info=True)
|
|
31
|
-
return InboundFrame(
|
|
32
|
-
opcode=0, cmd=0, seq=None, payload=None, raw=None
|
|
33
|
-
)
|
|
27
|
+
return InboundFrame(opcode=0, cmd=0, seq=None, payload=None, raw=None)
|
pymax/session/protocol.py
CHANGED
|
@@ -8,11 +8,7 @@ class StoreProtocol(Protocol):
|
|
|
8
8
|
async def save_session(self, session_info: SessionInfo) -> None: ...
|
|
9
9
|
async def update_token(self, old_token: str, new_token: str) -> None: ...
|
|
10
10
|
async def load_session(self) -> SessionInfo | None: ...
|
|
11
|
-
async def load_session_by_device_id(
|
|
12
|
-
|
|
13
|
-
) -> SessionInfo | None: ...
|
|
14
|
-
async def load_session_by_phone(
|
|
15
|
-
self, phone: str
|
|
16
|
-
) -> SessionInfo | None: ...
|
|
11
|
+
async def load_session_by_device_id(self, device_id: str) -> SessionInfo | None: ...
|
|
12
|
+
async def load_session_by_phone(self, phone: str) -> SessionInfo | None: ...
|
|
17
13
|
async def delete_session(self, token: str) -> None: ...
|
|
18
14
|
async def close(self) -> None: ...
|
pymax/session/store.py
CHANGED
|
@@ -55,24 +55,12 @@ class SessionStore:
|
|
|
55
55
|
)
|
|
56
56
|
"""
|
|
57
57
|
)
|
|
58
|
-
await self._ensure_column(
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
await self._ensure_column(
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
await self._ensure_column(
|
|
65
|
-
conn, "contacts_sync", "INTEGER NOT NULL DEFAULT -1"
|
|
66
|
-
)
|
|
67
|
-
await self._ensure_column(
|
|
68
|
-
conn, "drafts_sync", "INTEGER NOT NULL DEFAULT -1"
|
|
69
|
-
)
|
|
70
|
-
await self._ensure_column(
|
|
71
|
-
conn, "presence_sync", "INTEGER NOT NULL DEFAULT -1"
|
|
72
|
-
)
|
|
73
|
-
await self._ensure_column(
|
|
74
|
-
conn, "config_hash", "TEXT NOT NULL DEFAULT ''"
|
|
75
|
-
)
|
|
58
|
+
await self._ensure_column(conn, "mt_instance_id", "TEXT NOT NULL DEFAULT ''")
|
|
59
|
+
await self._ensure_column(conn, "chats_sync", "INTEGER NOT NULL DEFAULT -1")
|
|
60
|
+
await self._ensure_column(conn, "contacts_sync", "INTEGER NOT NULL DEFAULT -1")
|
|
61
|
+
await self._ensure_column(conn, "drafts_sync", "INTEGER NOT NULL DEFAULT -1")
|
|
62
|
+
await self._ensure_column(conn, "presence_sync", "INTEGER NOT NULL DEFAULT -1")
|
|
63
|
+
await self._ensure_column(conn, "config_hash", "TEXT NOT NULL DEFAULT ''")
|
|
76
64
|
await conn.execute(
|
|
77
65
|
"""
|
|
78
66
|
UPDATE sessions
|
|
@@ -93,9 +81,7 @@ class SessionStore:
|
|
|
93
81
|
columns = {row["name"] for row in await cursor.fetchall()}
|
|
94
82
|
|
|
95
83
|
if name not in columns:
|
|
96
|
-
await conn.execute(
|
|
97
|
-
f"ALTER TABLE sessions ADD COLUMN {name} {definition}"
|
|
98
|
-
)
|
|
84
|
+
await conn.execute(f"ALTER TABLE sessions ADD COLUMN {name} {definition}")
|
|
99
85
|
|
|
100
86
|
async def save_session(self, session_info: SessionInfo) -> None:
|
|
101
87
|
conn = await self._get_connection()
|
|
@@ -158,9 +144,7 @@ class SessionStore:
|
|
|
158
144
|
)
|
|
159
145
|
return self._row_to_session(row)
|
|
160
146
|
|
|
161
|
-
async def load_session_by_device_id(
|
|
162
|
-
self, device_id: str
|
|
163
|
-
) -> SessionInfo | None:
|
|
147
|
+
async def load_session_by_device_id(self, device_id: str) -> SessionInfo | None:
|
|
164
148
|
conn = await self._get_connection()
|
|
165
149
|
logger.debug("loading session by device_id=%s", device_id)
|
|
166
150
|
async with conn.execute(
|
pymax/telemetry/navigation.py
CHANGED
|
@@ -155,9 +155,7 @@ class NavigationPlanner:
|
|
|
155
155
|
self.current_screen = self.history.pop()
|
|
156
156
|
return self.current_screen
|
|
157
157
|
|
|
158
|
-
next_screen = self._weighted_choice(
|
|
159
|
-
self.rules.graph[self.current_screen]
|
|
160
|
-
)
|
|
158
|
+
next_screen = self._weighted_choice(self.rules.graph[self.current_screen])
|
|
161
159
|
if next_screen != self.current_screen:
|
|
162
160
|
self.history.append(self.current_screen)
|
|
163
161
|
if len(self.history) > 4:
|
pymax/telemetry/service.py
CHANGED
|
@@ -82,20 +82,14 @@ class TelemetryService:
|
|
|
82
82
|
async def _run(self) -> None:
|
|
83
83
|
try:
|
|
84
84
|
await asyncio.sleep(self._between(self._timing.startup_delay))
|
|
85
|
-
await self._send_events(
|
|
86
|
-
[self._payloads.login(self._user_id, self._session_id)]
|
|
87
|
-
)
|
|
85
|
+
await self._send_events([self._payloads.login(self._user_id, self._session_id)])
|
|
88
86
|
|
|
89
87
|
while True:
|
|
90
88
|
self._session_id += 1
|
|
91
|
-
events = await self._collect_session_events(
|
|
92
|
-
self._planner.new_profile()
|
|
93
|
-
)
|
|
89
|
+
events = await self._collect_session_events(self._planner.new_profile())
|
|
94
90
|
await self._send_events(events)
|
|
95
91
|
self._planner.reset_to_background()
|
|
96
|
-
await asyncio.sleep(
|
|
97
|
-
self._between(self._timing.session_idle_delay)
|
|
98
|
-
)
|
|
92
|
+
await asyncio.sleep(self._between(self._timing.session_idle_delay))
|
|
99
93
|
|
|
100
94
|
except asyncio.CancelledError:
|
|
101
95
|
raise
|
|
@@ -163,9 +157,7 @@ class TelemetryService:
|
|
|
163
157
|
except Exception:
|
|
164
158
|
logger.debug("telemetry send failed", exc_info=True)
|
|
165
159
|
|
|
166
|
-
def _nav_event(
|
|
167
|
-
self, screen_from: Screen, screen_to: Screen
|
|
168
|
-
) -> TelemetryEvent:
|
|
160
|
+
def _nav_event(self, screen_from: Screen, screen_to: Screen) -> TelemetryEvent:
|
|
169
161
|
event = self._payloads.navigation(
|
|
170
162
|
user_id=self._user_id,
|
|
171
163
|
session_id=self._session_id,
|
|
@@ -212,11 +204,7 @@ class TelemetryService:
|
|
|
212
204
|
|
|
213
205
|
@property
|
|
214
206
|
def _ready(self) -> bool:
|
|
215
|
-
return
|
|
216
|
-
self.app.started
|
|
217
|
-
and self.app.me is not None
|
|
218
|
-
and self.app.connection.is_open
|
|
219
|
-
)
|
|
207
|
+
return self.app.started and self.app.me is not None and self.app.connection.is_open
|
|
220
208
|
|
|
221
209
|
@property
|
|
222
210
|
def _user_id(self) -> int:
|
pymax/transport/tcp.py
CHANGED
|
@@ -10,9 +10,7 @@ logger = get_logger(__name__)
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TCPTransport(Transport):
|
|
13
|
-
def __init__(
|
|
14
|
-
self, host: str, port: int, proxy: str | None, use_ssl: bool = True
|
|
15
|
-
) -> None:
|
|
13
|
+
def __init__(self, host: str, port: int, proxy: str | None, use_ssl: bool = True) -> None:
|
|
16
14
|
self._host = host
|
|
17
15
|
self._port = port
|
|
18
16
|
self._proxy = proxy
|
|
@@ -11,9 +11,9 @@ class AudioAttachment(CamelModel):
|
|
|
11
11
|
"""Аудио-вложение сообщения.
|
|
12
12
|
|
|
13
13
|
:ivar duration: Длительность аудио.
|
|
14
|
-
:vartype duration: int
|
|
14
|
+
:vartype duration: int | None
|
|
15
15
|
:ivar audio_id: ID аудио.
|
|
16
|
-
:vartype audio_id: int
|
|
16
|
+
:vartype audio_id: int | None
|
|
17
17
|
:ivar wave: Данные waveform.
|
|
18
18
|
:vartype wave: str | None
|
|
19
19
|
:ivar transcription_status: Статус транскрибации.
|
|
@@ -26,8 +26,8 @@ class AudioAttachment(CamelModel):
|
|
|
26
26
|
:vartype token: str | None
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
duration: int
|
|
30
|
-
audio_id: int
|
|
29
|
+
duration: int | None = None
|
|
30
|
+
audio_id: int | None = None
|
|
31
31
|
wave: str | None = None
|
|
32
32
|
transcription_status: TranscriptionStatus | None = None
|
|
33
33
|
url: str | None = None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, model_validator
|
|
4
|
+
|
|
5
|
+
from pymax.types.domain.base import CamelModel
|
|
6
|
+
|
|
7
|
+
from .enums import AttachmentType
|
|
8
|
+
|
|
9
|
+
KNOWN_ATTACHMENT_TYPES = {
|
|
10
|
+
attachment_type.value
|
|
11
|
+
for attachment_type in AttachmentType
|
|
12
|
+
if attachment_type != AttachmentType.UNKNOWN
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UnknownAttachment(CamelModel):
|
|
17
|
+
"""Вложение неизвестного типа.
|
|
18
|
+
|
|
19
|
+
:ivar type: Тип вложения.
|
|
20
|
+
:vartype type: str
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
type: str = Field(alias="_type")
|
|
24
|
+
|
|
25
|
+
@model_validator(mode="before")
|
|
26
|
+
@classmethod
|
|
27
|
+
def reject_known_attachment_type(cls, value: Any) -> Any:
|
|
28
|
+
if not isinstance(value, dict):
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
attachment_type = value.get("_type", value.get("type"))
|
|
32
|
+
if attachment_type in KNOWN_ATTACHMENT_TYPES:
|
|
33
|
+
raise ValueError("Known attachment type should be parsed by its own model")
|
|
34
|
+
|
|
35
|
+
return value
|
|
@@ -31,7 +31,7 @@ class VideoAttachment(CamelModel):
|
|
|
31
31
|
:ivar video_id: ID видео.
|
|
32
32
|
:vartype video_id: int
|
|
33
33
|
:ivar duration: Длительность видео.
|
|
34
|
-
:vartype duration: int
|
|
34
|
+
:vartype duration: int | None
|
|
35
35
|
:ivar preview_data: Данные превью.
|
|
36
36
|
:vartype preview_data: bytes
|
|
37
37
|
:ivar type: Тип вложения.
|
|
@@ -47,7 +47,7 @@ class VideoAttachment(CamelModel):
|
|
|
47
47
|
height: int
|
|
48
48
|
width: int
|
|
49
49
|
video_id: int
|
|
50
|
-
duration: int
|
|
50
|
+
duration: int | None = None
|
|
51
51
|
preview_data: bytes
|
|
52
52
|
type: Literal[AttachmentType.VIDEO] = Field(alias="_type")
|
|
53
53
|
thumbnail: str
|
pymax/types/domain/auth.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field
|
|
2
2
|
|
|
3
|
+
from pymax.api.auth.enums import AuthType
|
|
3
4
|
from pymax.api.models import CamelModel
|
|
4
5
|
|
|
6
|
+
from .profile import Profile
|
|
7
|
+
|
|
5
8
|
|
|
6
9
|
class StartAuthResponse(CamelModel):
|
|
7
10
|
"""Ответ на начало авторизации.
|
|
@@ -70,7 +73,7 @@ class CheckCodeResponse(CamelModel):
|
|
|
70
73
|
:vartype password_challenge: PasswordChallenge | None
|
|
71
74
|
"""
|
|
72
75
|
|
|
73
|
-
token_attrs: TokenAttrs = Field(default_factory=TokenAttrs)
|
|
76
|
+
token_attrs: TokenAttrs = Field(default_factory=lambda: TokenAttrs.model_validate({}))
|
|
74
77
|
password_challenge: PasswordChallenge | None = None
|
|
75
78
|
|
|
76
79
|
@property
|
|
@@ -103,7 +106,7 @@ class CheckPasswordResponse(CamelModel):
|
|
|
103
106
|
:vartype error: str | None
|
|
104
107
|
"""
|
|
105
108
|
|
|
106
|
-
token_attrs: TokenAttrs = Field(default_factory=TokenAttrs)
|
|
109
|
+
token_attrs: TokenAttrs = Field(default_factory=lambda: TokenAttrs.model_validate({}))
|
|
107
110
|
error: str | None = None
|
|
108
111
|
|
|
109
112
|
@property
|
|
@@ -159,3 +162,22 @@ class CheckQrResponse(CamelModel):
|
|
|
159
162
|
"""
|
|
160
163
|
|
|
161
164
|
status: QrStatus
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ConfirmRegistrationResponse(CamelModel):
|
|
168
|
+
"""Ответ Max после регистрации нового аккаунта.
|
|
169
|
+
|
|
170
|
+
:ivar user_token: Внутренний ID зарегистрированного пользователя.
|
|
171
|
+
:vartype user_token: int
|
|
172
|
+
:ivar profile: Профиль зарегистрированного аккаунта.
|
|
173
|
+
:vartype profile: Profile
|
|
174
|
+
:ivar token_type: Тип выданного токена.
|
|
175
|
+
:vartype token_type: AuthType
|
|
176
|
+
:ivar token: Токен входа для новой сессии.
|
|
177
|
+
:vartype token: str
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
user_token: int
|
|
181
|
+
profile: Profile
|
|
182
|
+
token_type: AuthType
|
|
183
|
+
token: str
|
pymax/types/domain/chat.py
CHANGED
|
@@ -19,7 +19,8 @@ class Chat(CamelModel):
|
|
|
19
19
|
|
|
20
20
|
Объекты чатов, полученные через клиент, обычно уже привязаны к сервисам
|
|
21
21
|
сообщений и чатов. После этого можно вызывать удобные методы объекта:
|
|
22
|
-
:meth:`answer`, :meth:`history`, :meth:`
|
|
22
|
+
:meth:`answer`, :meth:`history`, :meth:`get_message`,
|
|
23
|
+
:meth:`get_messages`, :meth:`leave`, :meth:`invite`,
|
|
23
24
|
:meth:`remove_users`, :meth:`pin_message`, :meth:`update_settings` и
|
|
24
25
|
:meth:`rework_invite_link`.
|
|
25
26
|
|
|
@@ -147,6 +148,10 @@ class Chat(CamelModel):
|
|
|
147
148
|
"""
|
|
148
149
|
self._message_actions = message_actions
|
|
149
150
|
self._chat_actions = chat_actions
|
|
151
|
+
if self.last_message is not None:
|
|
152
|
+
self.last_message.bind(message_actions)
|
|
153
|
+
if self.pinned_message is not None:
|
|
154
|
+
self.pinned_message.bind(message_actions)
|
|
150
155
|
return self
|
|
151
156
|
|
|
152
157
|
async def answer(
|
|
@@ -242,6 +247,38 @@ class Chat(CamelModel):
|
|
|
242
247
|
interactive=interactive,
|
|
243
248
|
)
|
|
244
249
|
|
|
250
|
+
async def get_message(self, message_id: int) -> Message | None:
|
|
251
|
+
"""Возвращает сообщение этого чата по ID.
|
|
252
|
+
|
|
253
|
+
:param message_id: ID сообщения.
|
|
254
|
+
:type message_id: int
|
|
255
|
+
:returns: Сообщение или ``None``, если сервер его не вернул.
|
|
256
|
+
:rtype: Message | None
|
|
257
|
+
:raises RuntimeError: Если чат не привязан к клиенту.
|
|
258
|
+
"""
|
|
259
|
+
actions, _ = self._bound()
|
|
260
|
+
|
|
261
|
+
return await actions.get_message(
|
|
262
|
+
chat_id=self.id,
|
|
263
|
+
message_id=message_id,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
async def get_messages(self, message_ids: list[int]) -> list[Message]:
|
|
267
|
+
"""Возвращает сообщения этого чата по ID.
|
|
268
|
+
|
|
269
|
+
:param message_ids: ID сообщений.
|
|
270
|
+
:type message_ids: list[int]
|
|
271
|
+
:returns: Список найденных сообщений.
|
|
272
|
+
:rtype: list[Message]
|
|
273
|
+
:raises RuntimeError: Если чат не привязан к клиенту.
|
|
274
|
+
"""
|
|
275
|
+
actions, _ = self._bound()
|
|
276
|
+
|
|
277
|
+
return await actions.get_messages(
|
|
278
|
+
chat_id=self.id,
|
|
279
|
+
message_ids=message_ids,
|
|
280
|
+
)
|
|
281
|
+
|
|
245
282
|
async def leave(self) -> None:
|
|
246
283
|
"""Выходит из группы или канала.
|
|
247
284
|
|
pymax/types/domain/element.py
CHANGED
|
@@ -4,7 +4,7 @@ from .base import CamelModel
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class ElementAttributes(CamelModel):
|
|
7
|
-
url: str
|
|
7
|
+
url: str | None = None
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Element(CamelModel):
|
|
@@ -15,10 +15,10 @@ class Element(CamelModel):
|
|
|
15
15
|
:ivar from_: Начальная позиция элемента.
|
|
16
16
|
:vartype from_: int | None
|
|
17
17
|
:ivar length: Длина элемента.
|
|
18
|
-
:vartype length: int
|
|
18
|
+
:vartype length: int | None
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
21
|
type: str
|
|
22
22
|
from_: int | None = Field(serialization_alias="from", default=None)
|
|
23
|
-
length: int
|
|
23
|
+
length: int | None = None
|
|
24
24
|
attributes: ElementAttributes | None = None
|