maxapi-python 2.3.0__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 (61) hide show
  1. {maxapi_python-2.3.0.dist-info → maxapi_python-2.4.0.dist-info}/METADATA +4 -1
  2. {maxapi_python-2.3.0.dist-info → maxapi_python-2.4.0.dist-info}/RECORD +61 -53
  3. {maxapi_python-2.3.0.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 +33 -5
  14. pymax/api/messages/service.py +95 -13
  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 +65 -9
  40. pymax/infra/self.py +25 -1
  41. pymax/protocol/enums.py +13 -0
  42. pymax/protocol/tcp/compression.py +18 -0
  43. pymax/protocol/tcp/payload.py +41 -5
  44. pymax/protocol/tcp/protocol.py +5 -1
  45. pymax/transport/tcp.py +6 -1
  46. pymax/transport/websocket.py +6 -3
  47. pymax/types/domain/__init__.py +2 -1
  48. pymax/types/domain/attachments/__init__.py +2 -1
  49. pymax/types/domain/attachments/enums.py +17 -1
  50. pymax/types/domain/attachments/poll.py +111 -0
  51. pymax/types/domain/attachments/sticker.py +1 -1
  52. pymax/types/domain/attachments/video.py +29 -8
  53. pymax/types/domain/chat.py +9 -9
  54. pymax/types/domain/handshake.py +16 -0
  55. pymax/types/domain/login.py +30 -1
  56. pymax/types/domain/message.py +47 -19
  57. pymax/types/domain/user.py +5 -5
  58. pymax/types/events/__init__.py +1 -0
  59. pymax/types/events/reaction.py +2 -2
  60. pymax/types/events/voice.py +5 -0
  61. {maxapi_python-2.3.0.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())
@@ -1,4 +1,4 @@
1
- from typing import Any
1
+ from typing import Any, Literal
2
2
 
3
3
  from pydantic import Field
4
4
 
@@ -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
 
@@ -46,6 +47,26 @@ class SendMessagePayload(CamelModel):
46
47
  notify: bool = False
47
48
 
48
49
 
50
+ class ForwardLink(CamelModel):
51
+ type: Literal["FORWARD"] = "FORWARD"
52
+ message_id: str
53
+ chat_id: int
54
+
55
+
56
+ class ForwardMessagePayloadMessage(CamelModel):
57
+ cid: int
58
+ link: ForwardLink
59
+ attaches: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload] = Field(
60
+ default_factory=list
61
+ )
62
+
63
+
64
+ class ForwardMessagePayload(CamelModel):
65
+ chat_id: int
66
+ message: ForwardMessagePayloadMessage
67
+ notify: bool = True
68
+
69
+
49
70
  class ChatHistoryPayload(CamelModel):
50
71
  chat_id: int
51
72
  forward: int
@@ -109,3 +130,10 @@ class ReadMessagesPayload(CamelModel):
109
130
  chat_id: int
110
131
  message_id: str | int # Сокет просит int а вс str
111
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,
@@ -36,6 +38,9 @@ from .payloads import (
36
38
  ChatHistoryPayload,
37
39
  DeleteMessagePayload,
38
40
  EditMessagePayload,
41
+ ForwardLink,
42
+ ForwardMessagePayload,
43
+ ForwardMessagePayloadMessage,
39
44
  GetFilePayload,
40
45
  GetMessagesPayload,
41
46
  GetReactionsPayload,
@@ -47,12 +52,13 @@ from .payloads import (
47
52
  ReplyLink,
48
53
  SendMessagePayload,
49
54
  SendMessagePayloadMessage,
55
+ VotePollPayload,
50
56
  )
51
57
 
52
58
  if TYPE_CHECKING:
53
59
  from pymax.app import App
54
60
 
55
- SendAttachment: TypeAlias = Photo | File | Video
61
+ SendAttachment: TypeAlias = Photo | File | Video | Poll | Voice | VideoNote
56
62
  SendAttachments: TypeAlias = Sequence[SendAttachment] | None
57
63
 
58
64
  logger = get_logger(__name__)
@@ -73,13 +79,21 @@ class MessageService:
73
79
 
74
80
  async def _upload_attachments(
75
81
  self, attachments: SendAttachments
76
- ) -> list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload]:
77
- result: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload] = []
82
+ ) -> list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | Poll]:
83
+ result: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload | Poll] = []
78
84
  if not attachments:
79
85
  return result
80
86
 
81
87
  for attachment in attachments:
82
- 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):
83
97
  upload_result = await self.app.api.uploads.upload_photo(attachment)
84
98
  if not upload_result:
85
99
  logger.error("Photo uploading failed")
@@ -103,20 +117,30 @@ class MessageService:
103
117
 
104
118
  result.append(upload_result)
105
119
 
120
+ elif isinstance(attachment, Poll):
121
+ result.append(attachment)
122
+
106
123
  return result
107
124
 
108
125
  async def send_message(
109
126
  self,
110
127
  chat_id: int,
111
- text: str,
128
+ text: str | None = None,
112
129
  reply_to: int | None = None,
113
130
  attachments: SendAttachments = None,
114
131
  *,
115
132
  notify: bool = True,
116
- ) -> Message | None:
117
- 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")
118
139
 
119
- 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, []
120
144
 
121
145
  frame = SendMessagePayload(
122
146
  chat_id=chat_id,
@@ -139,6 +163,42 @@ class MessageService:
139
163
  logger.info("message sent chat_id=%s", chat_id)
140
164
  return message
141
165
 
166
+ async def forward_message(
167
+ self,
168
+ chat_id: int,
169
+ message_id: int | str,
170
+ source_chat_id: int | None = None,
171
+ *,
172
+ notify: bool = True,
173
+ ) -> Message:
174
+ source_chat_id = chat_id if source_chat_id is None else source_chat_id
175
+ logger.info(
176
+ "forwarding message source_chat_id=%s chat_id=%s message_id=%s",
177
+ source_chat_id,
178
+ chat_id,
179
+ message_id,
180
+ )
181
+
182
+ frame = ForwardMessagePayload(
183
+ chat_id=chat_id,
184
+ message=ForwardMessagePayloadMessage(
185
+ cid=-self._next_cid(),
186
+ link=ForwardLink(
187
+ message_id=str(message_id),
188
+ chat_id=source_chat_id,
189
+ ),
190
+ ),
191
+ notify=notify,
192
+ )
193
+
194
+ response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
195
+ message = bind_api_model(
196
+ self.app,
197
+ require_payload_model(response, Message),
198
+ )
199
+ logger.info("message forwarded source_chat_id=%s chat_id=%s", source_chat_id, chat_id)
200
+ return message
201
+
142
202
  async def get_messages(
143
203
  self,
144
204
  chat_id: int,
@@ -169,10 +229,18 @@ class MessageService:
169
229
  self,
170
230
  chat_id: int,
171
231
  message_id: int,
172
- text: str,
232
+ text: str | None = None,
173
233
  attachments: SendAttachments = None,
174
234
  ) -> Message:
175
- 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
+
176
244
  frame = EditMessagePayload(
177
245
  chat_id=chat_id,
178
246
  message_id=message_id,
@@ -204,7 +272,7 @@ class MessageService:
204
272
  get_chat: bool = False,
205
273
  get_messages: bool = True,
206
274
  interactive: bool = False,
207
- ) -> list[Message] | None:
275
+ ) -> list[Message]:
208
276
  frame = ChatHistoryPayload(
209
277
  chat_id=chat_id,
210
278
  forward=forward,
@@ -226,7 +294,7 @@ class MessageService:
226
294
  self.app,
227
295
  parse_payload_list(response, MessagePayloadKey.MESSAGES, Message),
228
296
  )
229
- return messages or None
297
+ return messages
230
298
 
231
299
  async def delete_message(
232
300
  self,
@@ -404,3 +472,17 @@ class MessageService:
404
472
 
405
473
  response = await self.app.invoke(Opcode.CHAT_MARK, frame.to_payload())
406
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_"