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.
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/METADATA +3 -1
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/RECORD +59 -51
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +7 -2
- pymax/_data/apk_fingerprints.json +682 -0
- pymax/api/auth/enums.py +1 -0
- pymax/api/auth/payloads.py +28 -2
- pymax/api/auth/service.py +88 -3
- pymax/api/chats/__init__.py +1 -0
- pymax/api/chats/enums.py +15 -0
- pymax/api/chats/payloads.py +16 -1
- pymax/api/chats/service.py +36 -1
- pymax/api/messages/payloads.py +12 -4
- pymax/api/messages/service.py +57 -14
- pymax/api/self/__init__.py +2 -1
- pymax/api/self/enums.py +9 -0
- pymax/api/self/payloads.py +40 -1
- pymax/api/self/service.py +37 -0
- pymax/api/session/service.py +21 -8
- pymax/api/uploads/payloads.py +6 -0
- pymax/api/uploads/service.py +198 -21
- pymax/app.py +69 -15
- pymax/base.py +8 -0
- pymax/client_web.py +2 -2
- pymax/config.py +44 -7
- pymax/connection/connection.py +29 -18
- pymax/dispatch/enums.py +1 -0
- pymax/dispatch/mapping.py +3 -1
- pymax/dispatch/resolvers.py +7 -1
- pymax/files/__init__.py +4 -1
- pymax/files/video.py +58 -0
- pymax/files/voice.py +39 -0
- pymax/fingerprint/__init__.py +1 -0
- pymax/fingerprint/fingerprint.py +47 -0
- pymax/fingerprint/models.py +11 -0
- pymax/infra/auth.py +4 -0
- pymax/infra/bots.py +3 -3
- pymax/infra/chat.py +38 -0
- pymax/infra/message.py +40 -11
- pymax/infra/self.py +25 -1
- pymax/protocol/enums.py +13 -0
- pymax/protocol/tcp/payload.py +21 -1
- pymax/transport/tcp.py +6 -1
- pymax/transport/websocket.py +6 -3
- pymax/types/domain/__init__.py +2 -1
- pymax/types/domain/attachments/__init__.py +2 -1
- pymax/types/domain/attachments/enums.py +17 -1
- pymax/types/domain/attachments/poll.py +111 -0
- pymax/types/domain/attachments/sticker.py +1 -1
- pymax/types/domain/attachments/video.py +29 -8
- pymax/types/domain/chat.py +9 -9
- pymax/types/domain/handshake.py +16 -0
- pymax/types/domain/login.py +30 -1
- pymax/types/domain/message.py +21 -20
- pymax/types/domain/user.py +1 -3
- pymax/types/events/__init__.py +1 -0
- pymax/types/events/reaction.py +2 -2
- pymax/types/events/voice.py +5 -0
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/licenses/LICENSE +0 -0
pymax/api/auth/enums.py
CHANGED
pymax/api/auth/payloads.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
)
|
pymax/api/chats/__init__.py
CHANGED
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"
|
pymax/api/chats/payloads.py
CHANGED
|
@@ -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
|
pymax/api/chats/service.py
CHANGED
|
@@ -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())
|
pymax/api/messages/payloads.py
CHANGED
|
@@ -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]
|
pymax/api/messages/service.py
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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]
|
|
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
|
|
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)
|
pymax/api/self/__init__.py
CHANGED
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_"
|
pymax/api/self/payloads.py
CHANGED
|
@@ -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
|