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
pymax/api/auth/enums.py CHANGED
@@ -6,6 +6,7 @@ class AuthType(str, Enum):
6
6
  CHECK_CODE = "CHECK_CODE"
7
7
  REGISTER = "REGISTER"
8
8
  RESEND = "RESEND"
9
+ LOGIN = "LOGIN"
9
10
 
10
11
 
11
12
  class ProfileOptions(int, Enum):
@@ -10,12 +10,13 @@ from .enums import AuthType, TwoFactorAction
10
10
  class RequestCodePayload(CamelModel):
11
11
  phone: str
12
12
  type: AuthType = AuthType.START_AUTH
13
- language: str = "ru"
13
+ mode: bytes | None
14
14
 
15
15
 
16
16
  class SendCodePayload(CamelModel):
17
17
  token: str
18
18
  verify_code: str
19
+
19
20
  auth_token_type: AuthType = AuthType.CHECK_CODE
20
21
 
21
22
 
@@ -42,19 +43,21 @@ class WebSyncPayload(CamelModel):
42
43
  cls,
43
44
  token: str,
44
45
  sync: SyncState,
46
+ interactive: bool = True,
45
47
  ) -> "WebSyncPayload":
46
48
  return cls(
47
49
  token=token,
48
50
  chats_sync=sync.chats_sync,
49
51
  contacts_sync=sync.contacts_sync,
50
52
  drafts_sync=sync.drafts_sync,
53
+ interactive=interactive,
51
54
  )
52
55
 
53
56
 
54
57
  class SyncPayload(CamelModel):
55
58
  user_agent: MobileUserAgentPayload
56
59
  token: str
57
- chat_hash_fingerprint: str | None = None
60
+ chat_cache_fingerprint: bytes | None = None
58
61
  chats_count: int | None = None
59
62
  chats_sync: int = -1
60
63
  contacts_sync: int = -1
@@ -70,15 +73,19 @@ class SyncPayload(CamelModel):
70
73
  user_agent: MobileUserAgentPayload,
71
74
  token: str,
72
75
  sync: SyncState,
76
+ chat_cache_fingerprint: bytes | None = None,
77
+ interactive: bool = True,
73
78
  ) -> "SyncPayload":
74
79
  return cls(
75
80
  user_agent=user_agent,
76
81
  token=token,
82
+ chat_cache_fingerprint=chat_cache_fingerprint,
77
83
  chats_sync=sync.chats_sync,
78
84
  contacts_sync=sync.contacts_sync,
79
85
  drafts_sync=sync.drafts_sync,
80
86
  presence_sync=sync.presence_sync,
81
87
  config_hash=sync.config_hash,
88
+ interactive=interactive,
82
89
  )
83
90
 
84
91
 
@@ -138,3 +145,22 @@ class ConfirmRegistrationPayload(CamelModel):
138
145
  last_name: str | None = None
139
146
  token: str
140
147
  token_type: AuthType = AuthType.REGISTER
148
+
149
+
150
+ class Login2Payload(CamelModel):
151
+ need_profile: bool
152
+ contacts_sync: int
153
+ config_hash: ConfigHash
154
+
155
+ @classmethod
156
+ def from_sync_state(
157
+ cls,
158
+ sync: SyncState,
159
+ profile_enabled: bool,
160
+ contact_enabled: bool,
161
+ ) -> "Login2Payload":
162
+ return cls(
163
+ need_profile=profile_enabled,
164
+ contacts_sync=sync.contacts_sync if contact_enabled else -1,
165
+ config_hash=sync.config_hash,
166
+ )
pymax/api/auth/service.py CHANGED
@@ -13,6 +13,7 @@ from pymax.auth import EmailCodeProvider
13
13
  from pymax.auth.providers import ConsoleEmailCodeProvider
14
14
  from pymax.logging import get_logger
15
15
  from pymax.protocol import Opcode
16
+ from pymax.types.domain import Login2Flags, Login2Response
16
17
  from pymax.types.domain.auth import (
17
18
  CheckCodeResponse,
18
19
  CheckPasswordResponse,
@@ -31,6 +32,7 @@ from .payloads import (
31
32
  ConfirmQrPayload,
32
33
  ConfirmRegistrationPayload,
33
34
  CreateAuthTrackPayload,
35
+ Login2Payload,
34
36
  MobileUserAgentPayload,
35
37
  RemoveTwoFactorPayload,
36
38
  RequestCodePayload,
@@ -58,7 +60,32 @@ class AuthService:
58
60
 
59
61
  async def request_code(self, phone: str) -> StartAuthResponse:
60
62
  logger.info("requesting sms code phone_set=%s", bool(phone))
61
- frame = RequestCodePayload(phone=phone)
63
+
64
+ if not self.app.handshake_response:
65
+ logger.error("request_code requested without handshake response")
66
+ raise RuntimeError("No handshake response available for request_code")
67
+
68
+ device_id = (
69
+ self.app.session.device_id if self.app.session else self.app.config.device.device_id
70
+ )
71
+
72
+ if self.app.config.device.user_agent.device_type != DeviceType.WEB:
73
+ if self.app.handshake_response.calls_seed is None:
74
+ raise ValueError(
75
+ "Unexpected internal state: handshake_response.calls_seed is missing "
76
+ + "in AuthService.request_code. Please report this issue to the developer."
77
+ )
78
+
79
+ mode = self.app.fingerprint_generator.generate_fingerprint(
80
+ version=self.app.config.device.user_agent.app_version,
81
+ device_id=device_id,
82
+ calls_seed=self.app.handshake_response.calls_seed,
83
+ arch=self.app.config.device.user_agent.arch or "arm64-v8a",
84
+ )
85
+ else:
86
+ mode = None
87
+
88
+ frame = RequestCodePayload(phone=phone, mode=mode)
62
89
  response = await self.app.invoke(Opcode.AUTH_REQUEST, frame.to_payload())
63
90
  logger.debug(
64
91
  "sms code request accepted payload_keys=%s",
@@ -72,7 +99,11 @@ class AuthService:
72
99
  bool(token),
73
100
  bool(verify_code),
74
101
  )
75
- frame = SendCodePayload(token=token, verify_code=verify_code)
102
+
103
+ frame = SendCodePayload(
104
+ token=token,
105
+ verify_code=verify_code,
106
+ )
76
107
  response = await self.app.invoke(Opcode.AUTH, frame.to_payload())
77
108
  logger.debug(
78
109
  "sms code response payload_keys=%s",
@@ -117,11 +148,35 @@ class AuthService:
117
148
  raise RuntimeError("No session available for login")
118
149
 
119
150
  logger.info("logging in")
151
+
152
+ if not self.app.handshake_response:
153
+ logger.error("login requested without handshake response")
154
+ raise RuntimeError("No handshake response available for login")
155
+
156
+ device_id = (
157
+ self.app.session.device_id if self.app.session else self.app.config.device.device_id
158
+ )
159
+
160
+ if self.app.handshake_response.calls_seed is None:
161
+ raise ValueError(
162
+ "Unexpected internal state: handshake_response.calls_seed is missing "
163
+ + "in AuthService.mobile_login. Please report this issue to the developer."
164
+ )
165
+
166
+ ccf = self.app.fingerprint_generator.generate_fingerprint(
167
+ version=self.app.config.device.user_agent.app_version,
168
+ device_id=device_id,
169
+ calls_seed=self.app.handshake_response.calls_seed,
170
+ arch=self.app.config.device.user_agent.arch or "arm64-v8a",
171
+ )
172
+
120
173
  sync = self.app.config.sync.resolve(session.sync)
121
174
  frame = SyncPayload.from_sync_state(
122
175
  user_agent=self.app.config.device.user_agent,
123
176
  token=session.token,
124
177
  sync=sync,
178
+ chat_cache_fingerprint=ccf,
179
+ interactive=self.app.config.interactive,
125
180
  )
126
181
  response = await self.app.invoke(Opcode.LOGIN, frame.to_payload())
127
182
 
@@ -134,6 +189,30 @@ class AuthService:
134
189
  await self._update_session(login_response)
135
190
  return login_response
136
191
 
192
+ async def mobile_login2(self, flags: Login2Flags) -> Login2Response:
193
+ session = self.app.session
194
+ if session is None:
195
+ logger.error("login2 requested without session")
196
+ raise RuntimeError("No session available for login2")
197
+
198
+ sync = self.app.config.sync.resolve(session.sync)
199
+
200
+ frame = Login2Payload.from_sync_state(
201
+ sync,
202
+ profile_enabled=flags.profile_enabled,
203
+ contact_enabled=flags.contact_enabled,
204
+ )
205
+
206
+ response = await self.app.invoke(Opcode.LOGIN2, frame.to_payload())
207
+ logger.debug("login2 response payload_keys=%s", payload_keys(response))
208
+
209
+ login2_response = bind_api_model(
210
+ self.app,
211
+ require_payload_model(response, Login2Response),
212
+ )
213
+ await self._update_session(login2_response)
214
+ return login2_response
215
+
137
216
  async def web_login(self) -> LoginResponse:
138
217
  session = self.app.session
139
218
  if session is None:
@@ -146,6 +225,7 @@ class AuthService:
146
225
  frame = WebSyncPayload.from_sync_state(
147
226
  token=session.token,
148
227
  sync=sync,
228
+ interactive=self.app.config.interactive,
149
229
  )
150
230
  response = await self.app.invoke(Opcode.LOGIN, frame.to_payload())
151
231
 
@@ -177,7 +257,7 @@ class AuthService:
177
257
 
178
258
  return require_payload_model(response, CheckCodeResponse)
179
259
 
180
- async def _update_session(self, response: LoginResponse) -> None:
260
+ async def _update_session(self, response: LoginResponse | Login2Response) -> None:
181
261
  session = self.app.session
182
262
  if session is None:
183
263
  return
@@ -377,3 +457,8 @@ class AuthService:
377
457
  response = await self.app.invoke(Opcode.AUTH_CONFIRM, frame.to_payload())
378
458
 
379
459
  return require_payload_model(response, ConfirmRegistrationResponse)
460
+
461
+ def is_update_available(self) -> bool:
462
+ return bool(
463
+ self.app.handshake_response.app_update_type if self.app.handshake_response else False
464
+ )
@@ -1,4 +1,5 @@
1
1
  from .enums import (
2
+ ChannelPermissions,
2
3
  ChatLinkPrefix,
3
4
  ChatMemberOperation,
4
5
  ChatOption,
pymax/api/chats/enums.py CHANGED
@@ -22,7 +22,22 @@ class ChatPayloadKey(str, Enum):
22
22
  CHAT = "chat"
23
23
  CHATS = "chats"
24
24
  MEMBERS = "members"
25
+ MARKER = "marker"
25
26
 
26
27
 
27
28
  class ChatLinkPrefix(str, Enum):
28
29
  JOIN = "join/"
30
+
31
+
32
+ class ChannelPermissions(int, Enum):
33
+ ADD_REMOVE_MEMBER = 2
34
+ ADD_ADMIN = 4
35
+ CHANGE_CHAT_INFO = 8
36
+ PIN_MESSAGE = 16
37
+ POST_MESSAGE = 256
38
+ EDIT_MESSAGE = 512
39
+ DELETE_MESSAGE = 1024
40
+
41
+
42
+ class PermType(str, Enum):
43
+ ADMIN = "ADMIN"
@@ -6,7 +6,7 @@ from pymax.api.models import CamelModel
6
6
  from pymax.types.domain.attachments.enums import AttachmentType
7
7
  from pymax.types.domain.enums import ChatType
8
8
 
9
- from .enums import ChatMemberOperation, ChatOption, ControlEvent
9
+ from .enums import ChatMemberOperation, ChatOption, ControlEvent, PermType
10
10
 
11
11
 
12
12
  class CreateGroupAttach(CamelModel):
@@ -95,6 +95,13 @@ class GetChatInfoPayload(CamelModel):
95
95
  chat_ids: list[int]
96
96
 
97
97
 
98
+ class GetChatMembersPayload(CamelModel):
99
+ type: str = "MEMBER" # TODO: ENUMM!!!
100
+ chat_id: int
101
+ marker: int
102
+ count: int = 50
103
+
104
+
98
105
  class LeaveChatPayload(CamelModel):
99
106
  chat_id: int
100
107
 
@@ -121,3 +128,11 @@ class DeleteChatPayload(CamelModel):
121
128
  chat_id: int
122
129
  last_event_time: int
123
130
  for_all: bool = True
131
+
132
+
133
+ class AddAdminPayload(CamelModel):
134
+ chat_id: int
135
+ user_ids: list[int]
136
+ type: PermType = PermType.ADMIN
137
+ operation: str = "add"
138
+ permissions: int
@@ -1,12 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import time
4
+ from functools import reduce
5
+ from operator import or_
4
6
  from typing import TYPE_CHECKING
5
7
 
6
8
  from pymax.api.binding import bind_api_model
7
9
  from pymax.api.response import (
8
10
  parse_payload_item_model,
9
11
  parse_payload_list,
12
+ payload_item,
10
13
  require_payload_item_model,
11
14
  require_payload_model,
12
15
  )
@@ -15,8 +18,9 @@ from pymax.logging import get_logger
15
18
  from pymax.protocol import Opcode
16
19
  from pymax.types.domain import Chat, Member, Message
17
20
 
18
- from .enums import ChatLinkPrefix, ChatMemberOperation, ChatPayloadKey
21
+ from .enums import ChannelPermissions, ChatLinkPrefix, ChatMemberOperation, ChatPayloadKey
19
22
  from .payloads import (
23
+ AddAdminPayload,
20
24
  ChangeGroupProfilePayload,
21
25
  ChangeGroupSettingsOptions,
22
26
  ChangeGroupSettingsPayload,
@@ -27,6 +31,7 @@ from .payloads import (
27
31
  FetchChatsPayload,
28
32
  FetchJoinRequests,
29
33
  GetChatInfoPayload,
34
+ GetChatMembersPayload,
30
35
  InviteUsersPayload,
31
36
  JoinChatPayload,
32
37
  JoinRequestActionPayload,
@@ -266,6 +271,22 @@ class ChatService:
266
271
 
267
272
  return [cached[chat_id] for chat_id in chat_ids if chat_id in cached]
268
273
 
274
+ async def get_chat_members(
275
+ self,
276
+ chat_id: int,
277
+ marker: int | None = None,
278
+ count: int = 50,
279
+ ) -> tuple[list[Member], int]:
280
+ frame = GetChatMembersPayload(chat_id=chat_id, marker=marker or 0, count=count)
281
+ response = await self.app.invoke(Opcode.CHAT_MEMBERS, frame.to_payload())
282
+
283
+ members = bind_api_model(
284
+ self.app,
285
+ parse_payload_list(response, ChatPayloadKey.MEMBERS, Member),
286
+ )
287
+ next_marker = payload_item(response, ChatPayloadKey.MARKER, int) or 0
288
+ return members, next_marker
289
+
269
290
  async def get_chat(self, chat_id: int) -> Chat:
270
291
  chats = await self.get_chats([chat_id])
271
292
  if not chats:
@@ -380,3 +401,17 @@ class ChatService:
380
401
 
381
402
  await self.app.invoke(Opcode.CHAT_DELETE, frame.to_payload())
382
403
  self._remove_cached_chat(chat_id)
404
+
405
+ async def add_admin(
406
+ self,
407
+ chat_id: int,
408
+ user_id: int,
409
+ permissions: list[ChannelPermissions],
410
+ ) -> None:
411
+ frame = AddAdminPayload(
412
+ chat_id=chat_id,
413
+ user_ids=[user_id],
414
+ permissions=reduce(or_, permissions),
415
+ )
416
+
417
+ await self.app.invoke(Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload())
@@ -8,6 +8,7 @@ from pymax.api.uploads.payloads import (
8
8
  AttachPhotoPayload,
9
9
  VideoAttachPayload,
10
10
  )
11
+ from pymax.types.domain import Poll
11
12
 
12
13
  from .enums import ItemType, ReadAction
13
14
 
@@ -20,9 +21,9 @@ class GetMessagesPayload(CamelModel):
20
21
  class EditMessagePayload(CamelModel):
21
22
  chat_id: int
22
23
  message_id: int
23
- text: str
24
+ text: str | None = None
24
25
  elements: list[Any]
25
- attachments: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload] = Field(
26
+ attachments: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | Poll] = Field(
26
27
  default_factory=list
27
28
  )
28
29
 
@@ -33,10 +34,10 @@ class ReplyLink(CamelModel):
33
34
 
34
35
 
35
36
  class SendMessagePayloadMessage(CamelModel):
36
- text: str
37
+ text: str | None = None
37
38
  cid: int
38
39
  elements: list[Any]
39
- attaches: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload]
40
+ attaches: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | Poll]
40
41
  link: ReplyLink | None = None
41
42
 
42
43
 
@@ -129,3 +130,10 @@ class ReadMessagesPayload(CamelModel):
129
130
  chat_id: int
130
131
  message_id: str | int # Сокет просит int а вс str
131
132
  mark: int
133
+
134
+
135
+ class VotePollPayload(CamelModel):
136
+ chat_id: int
137
+ message_id: int
138
+ poll_id: int
139
+ answers_ids: list[int]
@@ -18,13 +18,15 @@ from pymax.api.uploads.payloads import (
18
18
  VideoAttachPayload,
19
19
  )
20
20
  from pymax.exceptions import UploadError
21
- from pymax.files import File, Photo, Video
21
+ from pymax.files import File, Photo, Video, VideoNote, Voice
22
22
  from pymax.formatting.markdown import Formatter
23
23
  from pymax.logging import get_logger
24
24
  from pymax.protocol import Opcode
25
25
  from pymax.types.domain import (
26
26
  FileRequest,
27
27
  Message,
28
+ Poll,
29
+ PollState,
28
30
  ReactionInfo,
29
31
  ReadState,
30
32
  VideoRequest,
@@ -50,12 +52,13 @@ from .payloads import (
50
52
  ReplyLink,
51
53
  SendMessagePayload,
52
54
  SendMessagePayloadMessage,
55
+ VotePollPayload,
53
56
  )
54
57
 
55
58
  if TYPE_CHECKING:
56
59
  from pymax.app import App
57
60
 
58
- SendAttachment: TypeAlias = Photo | File | Video
61
+ SendAttachment: TypeAlias = Photo | File | Video | Poll | Voice | VideoNote
59
62
  SendAttachments: TypeAlias = Sequence[SendAttachment] | None
60
63
 
61
64
  logger = get_logger(__name__)
@@ -76,13 +79,21 @@ class MessageService:
76
79
 
77
80
  async def _upload_attachments(
78
81
  self, attachments: SendAttachments
79
- ) -> list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload]:
80
- result: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload] = []
82
+ ) -> list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | Poll]:
83
+ result: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | Poll] = []
81
84
  if not attachments:
82
85
  return result
83
86
 
84
87
  for attachment in attachments:
85
- if isinstance(attachment, Photo):
88
+ if isinstance(attachment, Voice):
89
+ upload_result = await self.app.api.uploads.upload_voice(attachment)
90
+ if not upload_result:
91
+ logger.error("Voice uploading failed")
92
+ raise UploadError("Voice uploading failed")
93
+
94
+ result.append(upload_result)
95
+
96
+ elif isinstance(attachment, Photo):
86
97
  upload_result = await self.app.api.uploads.upload_photo(attachment)
87
98
  if not upload_result:
88
99
  logger.error("Photo uploading failed")
@@ -106,20 +117,30 @@ class MessageService:
106
117
 
107
118
  result.append(upload_result)
108
119
 
120
+ elif isinstance(attachment, Poll):
121
+ result.append(attachment)
122
+
109
123
  return result
110
124
 
111
125
  async def send_message(
112
126
  self,
113
127
  chat_id: int,
114
- text: str,
128
+ text: str | None = None,
115
129
  reply_to: int | None = None,
116
130
  attachments: SendAttachments = None,
117
131
  *,
118
132
  notify: bool = True,
119
- ) -> Message | None:
120
- logger.info("sending message chat_id=%s text_len=%s", chat_id, len(text))
133
+ ) -> Message:
134
+ logger.info("sending message chat_id=%s text_len=%s", chat_id, len(text) if text else 0)
135
+
136
+ if not text and not attachments:
137
+ logger.error("send_message failed: no text or attachments provided")
138
+ raise ValueError("Either text or attachments must be provided")
121
139
 
122
- clean_text, elements = Formatter.format_markdown(text)
140
+ if text:
141
+ clean_text, elements = Formatter.format_markdown(text)
142
+ else:
143
+ clean_text, elements = None, []
123
144
 
124
145
  frame = SendMessagePayload(
125
146
  chat_id=chat_id,
@@ -149,7 +170,7 @@ class MessageService:
149
170
  source_chat_id: int | None = None,
150
171
  *,
151
172
  notify: bool = True,
152
- ) -> Message | None:
173
+ ) -> Message:
153
174
  source_chat_id = chat_id if source_chat_id is None else source_chat_id
154
175
  logger.info(
155
176
  "forwarding message source_chat_id=%s chat_id=%s message_id=%s",
@@ -208,10 +229,18 @@ class MessageService:
208
229
  self,
209
230
  chat_id: int,
210
231
  message_id: int,
211
- text: str,
232
+ text: str | None = None,
212
233
  attachments: SendAttachments = None,
213
234
  ) -> Message:
214
- clean_text, elements = Formatter.format_markdown(text)
235
+ if not text and not attachments:
236
+ logger.error("edit_message failed: no text or attachments provided")
237
+ raise ValueError("Either text or attachments must be provided")
238
+
239
+ if text:
240
+ clean_text, elements = Formatter.format_markdown(text)
241
+ else:
242
+ clean_text, elements = None, []
243
+
215
244
  frame = EditMessagePayload(
216
245
  chat_id=chat_id,
217
246
  message_id=message_id,
@@ -243,7 +272,7 @@ class MessageService:
243
272
  get_chat: bool = False,
244
273
  get_messages: bool = True,
245
274
  interactive: bool = False,
246
- ) -> list[Message] | None:
275
+ ) -> list[Message]:
247
276
  frame = ChatHistoryPayload(
248
277
  chat_id=chat_id,
249
278
  forward=forward,
@@ -265,7 +294,7 @@ class MessageService:
265
294
  self.app,
266
295
  parse_payload_list(response, MessagePayloadKey.MESSAGES, Message),
267
296
  )
268
- return messages or None
297
+ return messages
269
298
 
270
299
  async def delete_message(
271
300
  self,
@@ -443,3 +472,17 @@ class MessageService:
443
472
 
444
473
  response = await self.app.invoke(Opcode.CHAT_MARK, frame.to_payload())
445
474
  return require_payload_model(response, ReadState)
475
+
476
+ async def vote_poll(
477
+ self,
478
+ chat_id: int,
479
+ message_id: int,
480
+ poll_id: int,
481
+ answer_ids: list[int],
482
+ ) -> PollState:
483
+ frame = VotePollPayload(
484
+ chat_id=chat_id, message_id=message_id, poll_id=poll_id, answers_ids=answer_ids
485
+ )
486
+ response = await self.app.invoke(Opcode.SEND_VOTE, frame.to_payload())
487
+
488
+ return require_payload_item_model(response, "state", PollState)
@@ -1,2 +1,3 @@
1
- from .enums import AvatarType, SelfPayloadKey
1
+ from .enums import AvatarType, PrivacyAccess, SelfPayloadKey
2
+ from .payloads import PrivacySettingsUpdate
2
3
  from .service import SelfService
pymax/api/self/enums.py CHANGED
@@ -9,3 +9,12 @@ class SelfPayloadKey(str, Enum):
9
9
  PROFILE = "profile"
10
10
  URL = "url"
11
11
  TOKEN = "token"
12
+ HASH = "hash"
13
+
14
+
15
+ class PrivacyAccess(str, Enum):
16
+ """Уровень доступа к данным и действиям аккаунта."""
17
+
18
+ ALL = "ALL"
19
+ CONTACTS = "CONTACTS"
20
+ NOBODY = "_NONE_"
@@ -1,8 +1,10 @@
1
1
  from typing import Any
2
2
 
3
+ from pydantic import BaseModel, Field
4
+
3
5
  from pymax.api.models import CamelModel
4
6
 
5
- from .enums import AvatarType
7
+ from .enums import AvatarType, PrivacyAccess
6
8
 
7
9
 
8
10
  class UploadPayload(CamelModel):
@@ -39,3 +41,40 @@ class UpdateFolderPayload(CamelModel):
39
41
 
40
42
  class DeleteFolderPayload(CamelModel):
41
43
  folder_ids: list[str]
44
+
45
+
46
+ class PrivacySettingsUpdate(BaseModel):
47
+ """Изменяемые настройки приватности аккаунта.
48
+
49
+ Все поля необязательны: в запрос попадают только переданные настройки.
50
+
51
+ Args:
52
+ search_by_phone: Кто может найти аккаунт по номеру телефона.
53
+ incoming_calls: Кто может звонить аккаунту.
54
+ chat_invites: Кто может добавлять аккаунт в чаты.
55
+ phone_number_visibility: Кто может видеть номер телефона.
56
+ hide_online_status: Скрывать ли статус присутствия.
57
+ safe_content_only: Показывать только безопасный контент.
58
+ """
59
+
60
+ search_by_phone: PrivacyAccess | None = Field(
61
+ default=None, serialization_alias="SEARCH_BY_PHONE"
62
+ )
63
+ incoming_calls: PrivacyAccess | None = Field(default=None, serialization_alias="INCOMING_CALL")
64
+ chat_invites: PrivacyAccess | None = Field(default=None, serialization_alias="CHATS_INVITE")
65
+ phone_number_visibility: PrivacyAccess | None = Field(
66
+ default=None, serialization_alias="PHONE_NUMBER_PRIVACY"
67
+ )
68
+
69
+ hide_online_status: bool | None = Field(default=None, serialization_alias="HIDDEN")
70
+ safe_content_only: bool | None = Field(
71
+ default=None, serialization_alias="CONTENT_LEVEL_ACCESS"
72
+ )
73
+
74
+
75
+ class ChangeProfileSettings(CamelModel):
76
+ user: PrivacySettingsUpdate
77
+
78
+
79
+ class ChangeProfileSettingsPayload(CamelModel):
80
+ settings: ChangeProfileSettings