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.
Files changed (66) hide show
  1. {maxapi_python-2.1.2.dist-info → maxapi_python-2.2.0.dist-info}/METADATA +3 -11
  2. {maxapi_python-2.1.2.dist-info → maxapi_python-2.2.0.dist-info}/RECORD +66 -60
  3. pymax/__init__.py +18 -3
  4. pymax/api/auth/payloads.py +7 -0
  5. pymax/api/auth/service.py +33 -30
  6. pymax/api/binding.py +57 -0
  7. pymax/api/chats/service.py +34 -47
  8. pymax/api/messages/enums.py +1 -0
  9. pymax/api/messages/payloads.py +16 -1
  10. pymax/api/messages/service.py +85 -33
  11. pymax/api/models.py +4 -6
  12. pymax/api/response.py +2 -2
  13. pymax/api/self/service.py +17 -26
  14. pymax/api/session/payloads.py +2 -9
  15. pymax/api/session/service.py +1 -3
  16. pymax/api/uploads/payloads.py +3 -9
  17. pymax/api/uploads/service.py +33 -99
  18. pymax/api/users/service.py +8 -16
  19. pymax/app.py +20 -4
  20. pymax/auth/qr.py +3 -9
  21. pymax/auth/sms.py +23 -11
  22. pymax/base.py +38 -1
  23. pymax/client.py +3 -5
  24. pymax/client_web.py +1 -2
  25. pymax/config.py +42 -3
  26. pymax/connection/connection.py +48 -19
  27. pymax/connection/readers/tcp.py +1 -3
  28. pymax/dispatch/dispatcher.py +36 -18
  29. pymax/dispatch/enums.py +4 -0
  30. pymax/dispatch/mapping.py +34 -11
  31. pymax/dispatch/resolvers.py +18 -0
  32. pymax/dispatch/router.py +34 -0
  33. pymax/files/photo.py +4 -2
  34. pymax/formatting/markdown.py +22 -13
  35. pymax/infra/chat.py +12 -0
  36. pymax/infra/message.py +74 -3
  37. pymax/logging.py +35 -3
  38. pymax/protocol/tcp/compression.py +1 -3
  39. pymax/protocol/tcp/framing.py +1 -3
  40. pymax/protocol/tcp/payload.py +22 -42
  41. pymax/protocol/tcp/protocol.py +2 -8
  42. pymax/protocol/ws/protocol.py +3 -9
  43. pymax/session/protocol.py +2 -6
  44. pymax/session/store.py +8 -24
  45. pymax/telemetry/navigation.py +1 -3
  46. pymax/telemetry/service.py +5 -17
  47. pymax/transport/tcp.py +1 -3
  48. pymax/types/domain/attachments/__init__.py +1 -0
  49. pymax/types/domain/attachments/audio.py +4 -4
  50. pymax/types/domain/attachments/enums.py +1 -0
  51. pymax/types/domain/attachments/unknown.py +35 -0
  52. pymax/types/domain/attachments/video.py +2 -2
  53. pymax/types/domain/auth.py +24 -2
  54. pymax/types/domain/chat.py +38 -1
  55. pymax/types/domain/element.py +3 -3
  56. pymax/types/domain/message.py +34 -2
  57. pymax/types/domain/presence.py +3 -3
  58. pymax/types/domain/sync.py +5 -21
  59. pymax/types/events/__init__.py +4 -0
  60. pymax/types/events/mark.py +23 -0
  61. pymax/types/events/message.py +57 -5
  62. pymax/types/events/presence.py +15 -0
  63. pymax/types/events/reaction.py +21 -0
  64. pymax/types/events/typing.py +14 -0
  65. {maxapi_python-2.1.2.dist-info → maxapi_python-2.2.0.dist-info}/WHEEL +0 -0
  66. {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
- Вызывайте эту функцию вручную, если хотите управлять stream или цветами.
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(_normalize_level(level))
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())
@@ -1,7 +1,5 @@
1
1
  class Lz4BlockCompression:
2
- def decompress(
3
- self, src: bytes, max_output: int = 5 * 1024 * 1024
4
- ) -> bytes:
2
+ def decompress(self, src: bytes, max_output: int = 5 * 1024 * 1024) -> bytes:
5
3
  dst = bytearray()
6
4
  pos = 0
7
5
 
@@ -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
 
@@ -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(self, payload_bytes: bytes, *, raw: bool) -> list[Any]:
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) -> dict[Any, Any]:
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, raw=False, strict_map_key=False
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
- try:
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 payload has extra data objects=%s extra_bytes=%s",
65
- [type(value).__name__ for value in values],
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
- for value in values:
69
- if isinstance(value, dict):
70
- return value
71
- raise
72
- except UnicodeDecodeError:
73
- values = self._unpack_stream(payload_bytes, raw=True)
74
- logger.debug(
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):
@@ -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",
@@ -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
- "failed to decode websocket frame json", exc_info=True
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
- self, device_id: str
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
- conn, "mt_instance_id", "TEXT NOT NULL DEFAULT ''"
60
- )
61
- await self._ensure_column(
62
- conn, "chats_sync", "INTEGER NOT NULL DEFAULT -1"
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(
@@ -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:
@@ -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
@@ -8,4 +8,5 @@ from .keyboards import InlineKeyboardAttachment
8
8
  from .photo import PhotoAttachment
9
9
  from .share import ShareAttachment
10
10
  from .sticker import StickerAttachment
11
+ from .unknown import UnknownAttachment
11
12
  from .video import VideoAttachment, VideoRequest
@@ -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
@@ -14,6 +14,7 @@ class AttachmentType(str, Enum):
14
14
  CALL = "CALL"
15
15
  SHARE = "SHARE"
16
16
  INLINE_KEYBOARD = "INLINE_KEYBOARD"
17
+ UNKNOWN = "UNKNOWN"
17
18
 
18
19
 
19
20
  class TranscriptionStatus(str, Enum):
@@ -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
@@ -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
@@ -19,7 +19,8 @@ class Chat(CamelModel):
19
19
 
20
20
  Объекты чатов, полученные через клиент, обычно уже привязаны к сервисам
21
21
  сообщений и чатов. После этого можно вызывать удобные методы объекта:
22
- :meth:`answer`, :meth:`history`, :meth:`leave`, :meth:`invite`,
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
 
@@ -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