maxapi-python 2.3.1__py3-none-any.whl → 2.4.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 (59) hide show
  1. {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/METADATA +3 -1
  2. {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/RECORD +59 -51
  3. {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +7 -2
  5. pymax/_data/apk_fingerprints.json +682 -0
  6. pymax/api/auth/enums.py +1 -0
  7. pymax/api/auth/payloads.py +28 -2
  8. pymax/api/auth/service.py +88 -3
  9. pymax/api/chats/__init__.py +1 -0
  10. pymax/api/chats/enums.py +15 -0
  11. pymax/api/chats/payloads.py +16 -1
  12. pymax/api/chats/service.py +36 -1
  13. pymax/api/messages/payloads.py +12 -4
  14. pymax/api/messages/service.py +57 -14
  15. pymax/api/self/__init__.py +2 -1
  16. pymax/api/self/enums.py +9 -0
  17. pymax/api/self/payloads.py +40 -1
  18. pymax/api/self/service.py +37 -0
  19. pymax/api/session/service.py +21 -8
  20. pymax/api/uploads/payloads.py +6 -0
  21. pymax/api/uploads/service.py +198 -21
  22. pymax/app.py +69 -15
  23. pymax/base.py +8 -0
  24. pymax/client_web.py +2 -2
  25. pymax/config.py +44 -7
  26. pymax/connection/connection.py +29 -18
  27. pymax/dispatch/enums.py +1 -0
  28. pymax/dispatch/mapping.py +3 -1
  29. pymax/dispatch/resolvers.py +7 -1
  30. pymax/files/__init__.py +4 -1
  31. pymax/files/video.py +58 -0
  32. pymax/files/voice.py +39 -0
  33. pymax/fingerprint/__init__.py +1 -0
  34. pymax/fingerprint/fingerprint.py +47 -0
  35. pymax/fingerprint/models.py +11 -0
  36. pymax/infra/auth.py +4 -0
  37. pymax/infra/bots.py +3 -3
  38. pymax/infra/chat.py +38 -0
  39. pymax/infra/message.py +40 -11
  40. pymax/infra/self.py +25 -1
  41. pymax/protocol/enums.py +13 -0
  42. pymax/protocol/tcp/payload.py +21 -1
  43. pymax/transport/tcp.py +6 -1
  44. pymax/transport/websocket.py +6 -3
  45. pymax/types/domain/__init__.py +2 -1
  46. pymax/types/domain/attachments/__init__.py +2 -1
  47. pymax/types/domain/attachments/enums.py +17 -1
  48. pymax/types/domain/attachments/poll.py +111 -0
  49. pymax/types/domain/attachments/sticker.py +1 -1
  50. pymax/types/domain/attachments/video.py +29 -8
  51. pymax/types/domain/chat.py +9 -9
  52. pymax/types/domain/handshake.py +16 -0
  53. pymax/types/domain/login.py +30 -1
  54. pymax/types/domain/message.py +21 -20
  55. pymax/types/domain/user.py +1 -3
  56. pymax/types/events/__init__.py +1 -0
  57. pymax/types/events/reaction.py +2 -2
  58. pymax/types/events/voice.py +5 -0
  59. {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -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(raw=raw, strict_map_key=False)
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/transport/tcp.py CHANGED
@@ -7,6 +7,7 @@ from pymax.logging import get_logger
7
7
  from .base import Transport
8
8
 
9
9
  logger = get_logger(__name__)
10
+ _CLOSE_TIMEOUT = 5.0
10
11
 
11
12
 
12
13
  class TCPTransport(Transport):
@@ -66,7 +67,11 @@ class TCPTransport(Transport):
66
67
  if writer:
67
68
  logger.debug("tcp close")
68
69
  writer.close()
69
- await writer.wait_closed()
70
+ try:
71
+ await asyncio.wait_for(writer.wait_closed(), _CLOSE_TIMEOUT)
72
+ except (OSError, TimeoutError) as e:
73
+ logger.warning("tcp close did not finish cleanly: %s", e)
74
+ writer.transport.abort()
70
75
  logger.debug("tcp closed")
71
76
 
72
77
  async def send(self, data: bytes | str) -> None:
@@ -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, origin=Origin("https://web.max.ru"), proxy=self.proxy
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=True)
47
+ return await self.ws.recv(decode=False)
45
48
 
46
49
  @property
47
50
  def connected(self) -> bool:
@@ -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 .login import LoginResponse
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
@@ -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,18 @@ 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
@@ -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
@@ -42,7 +42,7 @@ class StickerAttachment(CamelModel):
42
42
  sticker_id: int
43
43
  tags: list[str] | None = None
44
44
  width: int
45
- set_id: int
45
+ set_id: int | None = None
46
46
  time: int
47
47
  sticker_type: str
48
48
  audio: bool
@@ -62,18 +62,18 @@ class VideoRequest(CamelModel):
62
62
  :vartype external: str | bool | None
63
63
  :ivar cache: Использовать ли кеш.
64
64
  :vartype cache: bool
65
- :ivar url: URL видео.
66
- :vartype url: str
65
+ :ivar url: Прямой URL видео или ``None`` для внешнего видео.
66
+ :vartype url: str | None
67
67
  """
68
68
 
69
69
  external: str | bool | None = Field(default=None, alias="EXTERNAL")
70
- cache: bool
71
- url: str
70
+ cache: bool = False # TODO: idk maybe | None = None better
71
+ url: str | None = None
72
72
 
73
73
  @model_validator(mode="before")
74
74
  @classmethod
75
- def unwrap_dynamic_url(cls, value: Any) -> Any:
76
- """Нормализует динамический ключ URL в поле ``url``.
75
+ def select_video_url(cls, value: Any) -> Any:
76
+ """Выбирает прямой URL с максимальным доступным MP4-качеством.
77
77
 
78
78
  :param value: Значение, переданное в валидатор модели.
79
79
  :type value: Any
@@ -83,8 +83,29 @@ class VideoRequest(CamelModel):
83
83
  if not isinstance(value, dict) or "url" in value:
84
84
  return value
85
85
 
86
+ mp4_urls: list[tuple[int, str]] = []
86
87
  for key, url in value.items():
87
- if key not in ("EXTERNAL", "cache"):
88
- return {**value, "url": url}
88
+ if not isinstance(key, str) or not isinstance(url, str):
89
+ continue
90
+
91
+ normalized_key = key.upper()
92
+ if not normalized_key.startswith("MP4_"):
93
+ continue
94
+
95
+ try:
96
+ quality = int(normalized_key.removeprefix("MP4_"))
97
+ except ValueError:
98
+ continue
99
+
100
+ if quality > 0:
101
+ mp4_urls.append((quality, url))
102
+
103
+ if mp4_urls:
104
+ _, url = max(mp4_urls, key=lambda item: item[0])
105
+ return {**value, "url": url}
106
+
107
+ legacy_url = value.get("dynamicUrl", value.get("dynamic_url"))
108
+ if isinstance(legacy_url, str):
109
+ return {**value, "url": legacy_url}
89
110
 
90
111
  return value
@@ -156,25 +156,24 @@ class Chat(CamelModel):
156
156
 
157
157
  async def answer(
158
158
  self,
159
- text: str,
159
+ text: str | None = None,
160
160
  reply_to: int | None = None,
161
161
  attachments: SendAttachments = None,
162
162
  *,
163
163
  notify: bool = True,
164
- ) -> Message | None:
164
+ ) -> Message:
165
165
  """Отправляет сообщение в этот чат.
166
166
 
167
167
  :param text: Текст сообщения.
168
- :type text: str
168
+ :type text: str | None
169
169
  :param reply_to: ID сообщения для ответа.
170
170
  :type reply_to: int | None
171
171
  :param attachments: Файлы, фотографии или видео для отправки.
172
172
  :type attachments: SendAttachments
173
173
  :param notify: Отправить ли получателям push-уведомление.
174
174
  :type notify: bool
175
- :returns: Отправленное сообщение или ``None``, если сервер не вернул
176
- его.
177
- :rtype: Message | None
175
+ :returns: Отправленное сообщение.
176
+ :rtype: Message
178
177
  :raises RuntimeError: Если чат не привязан к клиенту.
179
178
  """
180
179
  actions, _ = self._bound()
@@ -198,7 +197,7 @@ class Chat(CamelModel):
198
197
  get_chat: bool = False,
199
198
  get_messages: bool = True,
200
199
  interactive: bool = False,
201
- ) -> list[Message] | None:
200
+ ) -> list[Message]:
202
201
  """Загружает историю сообщений этого чата.
203
202
 
204
203
  ``from_time``, ``backward_time`` и ``forward_time`` передаются в
@@ -224,8 +223,9 @@ class Chat(CamelModel):
224
223
  :type get_messages: bool
225
224
  :param interactive: Пометить запрос как интерактивный.
226
225
  :type interactive: bool
227
- :returns: Сообщения или ``None``, если сервер не вернул список.
228
- :rtype: list[Message] | None
226
+ :returns: Список сообщений. Если сервер не вернул сообщения, список
227
+ пуст.
228
+ :rtype: list[Message]
229
229
  :raises RuntimeError: Если чат не привязан к клиенту.
230
230
  """
231
231
  actions, _ = self._bound()
@@ -0,0 +1,16 @@
1
+ from pydantic import Field
2
+
3
+ from .base import CamelModel
4
+
5
+
6
+ class HandshakeResponse(CamelModel):
7
+ """Результат рукопожатия.
8
+
9
+ :ivar calls_seed: Сид для генерации хэшей вызовов.
10
+ :vartype calls_seed: int
11
+ """
12
+
13
+ calls_seed: int | None = None
14
+ app_update_type: int | None = Field(
15
+ alias="app-update-type", default=None
16
+ ) # very bizarre casing
@@ -13,14 +13,26 @@ class LoginConfig(CamelModel):
13
13
  hash: ConfigHash | None = None
14
14
 
15
15
 
16
+ class Login2Flags(CamelModel):
17
+ config_enabled: bool = False
18
+ contact_enabled: bool = False
19
+ profile_enabled: bool = False
20
+
21
+ @property
22
+ def enabled(self) -> bool:
23
+ return self.config_enabled or self.contact_enabled or self.profile_enabled
24
+
25
+
16
26
  class LoginResponse(CamelModel):
17
27
  chats: list[Chat] = Field(default_factory=list)
18
- profile: Profile
28
+ profile: Profile | None = None
19
29
  messages: dict[int, list[Message]] = Field(default_factory=dict) # chat_id -> [message]
20
30
  contacts: list[User | None] = Field(default_factory=list)
21
31
  token: str | None = None
22
32
  time: int | None = None
23
33
  config: LoginConfig | None = None
34
+ updates: int | None = None
35
+ login2_flags: Login2Flags | None = None
24
36
 
25
37
  def update_sync_state(self, current: SyncState) -> SyncState:
26
38
  sync_time = self.time
@@ -33,3 +45,20 @@ class LoginResponse(CamelModel):
33
45
  presence_sync=(sync_time if sync_time is not None else current.presence_sync),
34
46
  config_hash=(config_hash if config_hash is not None else current.config_hash),
35
47
  )
48
+
49
+
50
+ class Login2Response(CamelModel):
51
+ profile: Profile | None = None
52
+ contacts: list[User | None] = Field(default_factory=list, alias="contactInfos")
53
+ config: LoginConfig | None = None
54
+
55
+ def update_sync_state(self, current: SyncState) -> SyncState:
56
+ config_hash = self.config.hash if self.config is not None else None
57
+
58
+ return SyncState(
59
+ chats_sync=current.chats_sync,
60
+ contacts_sync=current.contacts_sync,
61
+ drafts_sync=current.drafts_sync,
62
+ presence_sync=current.presence_sync,
63
+ config_hash=(config_hash if config_hash is not None else current.config_hash),
64
+ )
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Annotated, Any, TypeAlias
5
5
 
6
6
  from pydantic import Field, PrivateAttr, model_validator
7
7
 
8
- from pymax.files import File, Photo, Video
8
+ from pymax.files import File, Photo, Video, VideoNote, Voice
9
9
  from pymax.types.domain import (
10
10
  AudioAttachment,
11
11
  CallAttachment,
@@ -14,6 +14,8 @@ from pymax.types.domain import (
14
14
  FileAttachment,
15
15
  InlineKeyboardAttachment,
16
16
  PhotoAttachment,
17
+ Poll,
18
+ PollAttachment,
17
19
  ShareAttachment,
18
20
  StickerAttachment,
19
21
  UnknownAttachment,
@@ -38,11 +40,12 @@ KnownAttachment: TypeAlias = Annotated[
38
40
  | ControlAttachment
39
41
  | InlineKeyboardAttachment
40
42
  | ShareAttachment
41
- | CallAttachment,
43
+ | CallAttachment
44
+ | PollAttachment,
42
45
  Field(discriminator="type"),
43
46
  ]
44
47
  Attachment: TypeAlias = KnownAttachment | UnknownAttachment
45
- SendAttachment: TypeAlias = Photo | File | Video
48
+ SendAttachment: TypeAlias = Photo | File | Video | Poll | Voice | VideoNote
46
49
  SendAttachments: TypeAlias = Sequence[SendAttachment] | None
47
50
 
48
51
 
@@ -182,22 +185,21 @@ class Message(CamelModel):
182
185
 
183
186
  async def reply(
184
187
  self,
185
- text: str,
188
+ text: str | None = None,
186
189
  attachments: SendAttachments = None,
187
190
  *,
188
191
  notify: bool = True,
189
- ) -> Message | None:
192
+ ) -> Message:
190
193
  """Отправляет ответ на это сообщение в тот же чат.
191
194
 
192
195
  :param text: Текст сообщения.
193
- :type text: str
196
+ :type text: str | None
194
197
  :param attachments: Файлы, фотографии или видео для отправки.
195
198
  :type attachments: SendAttachments
196
199
  :param notify: Отправить ли получателям push-уведомление.
197
200
  :type notify: bool
198
- :returns: Отправленное сообщение или ``None``, если сервер не вернул
199
- его.
200
- :rtype: Message | None
201
+ :returns: Отправленное сообщение.
202
+ :rtype: Message
201
203
  :raises RuntimeError: Если сообщение не привязано к сервису или не
202
204
  содержит ``chat_id``.
203
205
  """
@@ -213,25 +215,24 @@ class Message(CamelModel):
213
215
 
214
216
  async def answer(
215
217
  self,
216
- text: str,
218
+ text: str | None = None,
217
219
  reply_to: int | None = None,
218
220
  attachments: SendAttachments = None,
219
221
  *,
220
222
  notify: bool = True,
221
- ) -> Message | None:
223
+ ) -> Message:
222
224
  """Отправляет сообщение в тот же чат.
223
225
 
224
226
  :param text: Текст сообщения.
225
- :type text: str
227
+ :type text: str | None
226
228
  :param reply_to: ID сообщения для ответа.
227
229
  :type reply_to: int | None
228
230
  :param attachments: Файлы, фотографии или видео для отправки.
229
231
  :type attachments: SendAttachments
230
232
  :param notify: Отправить ли получателям push-уведомление.
231
233
  :type notify: bool
232
- :returns: Отправленное сообщение или ``None``, если сервер не вернул
233
- его.
234
- :rtype: Message | None
234
+ :returns: Отправленное сообщение.
235
+ :rtype: Message
235
236
  :raises RuntimeError: Если сообщение не привязано к сервису или не
236
237
  содержит ``chat_id``.
237
238
  """
@@ -250,15 +251,15 @@ class Message(CamelModel):
250
251
  chat_id: int,
251
252
  *,
252
253
  notify: bool = True,
253
- ) -> Message | None:
254
+ ) -> Message:
254
255
  """Пересылает это сообщение в другой чат.
255
256
 
256
257
  :param chat_id: ID целевого чата.
257
258
  :type chat_id: int
258
259
  :param notify: Отправить ли получателям push-уведомление.
259
260
  :type notify: bool
260
- :returns: Пересланное сообщение или ``None``, если сервер его не вернул.
261
- :rtype: Message | None
261
+ :returns: Пересланное сообщение.
262
+ :rtype: Message
262
263
  :raises RuntimeError: Если сообщение не привязано к сервису или не
263
264
  содержит ``chat_id``.
264
265
  """
@@ -291,13 +292,13 @@ class Message(CamelModel):
291
292
 
292
293
  async def edit(
293
294
  self,
294
- text: str,
295
+ text: str | None = None,
295
296
  attachments: SendAttachments = None,
296
297
  ) -> Message:
297
298
  """Редактирует текст и вложения этого сообщения.
298
299
 
299
300
  :param text: Новый текст сообщения с поддержкой markdown.
300
- :type text: str
301
+ :type text: str | None
301
302
  :param attachments: Новые файлы, фотографии или видео для сообщения.
302
303
  :type attachments: SendAttachments
303
304
  :returns: Отредактированное сообщение.
@@ -71,10 +71,8 @@ class User(CamelModel):
71
71
  phone: int | None = None
72
72
  status: str | None = None
73
73
  description: str | None = None
74
- # Bots may send ``gender`` as a numeric code and ``web_app`` as a URL
75
- # string instead of an object; accept these so profile parsing won't fail.
76
74
  gender: str | int | None = None
77
- link: str | None = None
75
+ link: str | int | None = None
78
76
  web_app: dict[str, Any] | str | None = None
79
77
  menu_button: dict[str, Any] | None = None
80
78
 
@@ -5,3 +5,4 @@ from .presence import PresenceEvent
5
5
  from .reaction import ReactionUpdateEvent
6
6
  from .typing import TypingEvent
7
7
  from .video import VideoUploadSignal
8
+ from .voice import AudioUploadSignal
@@ -17,5 +17,5 @@ class ReactionUpdateEvent(CamelModel):
17
17
 
18
18
  message_id: str
19
19
  chat_id: int
20
- counters: list[ReactionCounter]
21
- total_count: int
20
+ counters: list[ReactionCounter] | None = None
21
+ total_count: int = 0
@@ -0,0 +1,5 @@
1
+ from pymax.types.domain.base import CamelModel
2
+
3
+
4
+ class AudioUploadSignal(CamelModel):
5
+ audio_id: int