maxapi-python 2.0.1__py3-none-any.whl → 2.1.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.0.1.dist-info → maxapi_python-2.1.0.dist-info}/METADATA +4 -1
  2. {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.0.dist-info}/RECORD +61 -54
  3. pymax/__init__.py +1 -1
  4. pymax/api/auth/enums.py +13 -2
  5. pymax/api/auth/payloads.py +10 -6
  6. pymax/api/auth/service.py +75 -12
  7. pymax/api/bots/__init__.py +1 -0
  8. pymax/api/bots/payloads.py +7 -0
  9. pymax/api/bots/service.py +35 -0
  10. pymax/api/chats/enums.py +1 -0
  11. pymax/api/chats/payloads.py +14 -0
  12. pymax/api/chats/service.py +112 -12
  13. pymax/api/facade.py +2 -0
  14. pymax/api/messages/payloads.py +5 -1
  15. pymax/api/messages/service.py +36 -11
  16. pymax/api/self/service.py +18 -6
  17. pymax/api/session/payloads.py +9 -2
  18. pymax/api/uploads/models.py +1 -4
  19. pymax/api/uploads/payloads.py +9 -3
  20. pymax/api/uploads/service.py +103 -35
  21. pymax/api/users/service.py +15 -5
  22. pymax/app.py +15 -5
  23. pymax/auth/qr.py +11 -6
  24. pymax/auth/sms.py +13 -4
  25. pymax/base.py +1 -0
  26. pymax/client.py +4 -1
  27. pymax/client_web.py +4 -2
  28. pymax/config.py +11 -3
  29. pymax/connection/connection.py +15 -5
  30. pymax/connection/readers/tcp.py +4 -2
  31. pymax/dispatch/dispatcher.py +28 -10
  32. pymax/dispatch/mapping.py +9 -2
  33. pymax/dispatch/router.py +2 -0
  34. pymax/files/base.py +6 -1
  35. pymax/formatting/markdown.py +4 -1
  36. pymax/infra/auth.py +42 -0
  37. pymax/infra/base.py +2 -0
  38. pymax/infra/bots.py +33 -0
  39. pymax/infra/chat.py +102 -1
  40. pymax/protocol/tcp/compression.py +3 -1
  41. pymax/protocol/tcp/framing.py +6 -11
  42. pymax/protocol/tcp/payload.py +3 -2
  43. pymax/protocol/tcp/protocol.py +13 -3
  44. pymax/protocol/ws/protocol.py +9 -3
  45. pymax/session/protocol.py +6 -2
  46. pymax/session/store.py +24 -8
  47. pymax/telemetry/navigation.py +3 -1
  48. pymax/telemetry/service.py +9 -3
  49. pymax/transport/tcp.py +10 -4
  50. pymax/transport/websocket.py +0 -2
  51. pymax/types/domain/__init__.py +3 -0
  52. pymax/types/domain/bots.py +14 -0
  53. pymax/types/domain/error.py +3 -3
  54. pymax/types/domain/folder.py +1 -1
  55. pymax/types/domain/login.py +18 -6
  56. pymax/types/domain/member.py +16 -0
  57. pymax/types/domain/presence.py +15 -0
  58. pymax/types/domain/sync.py +21 -5
  59. pymax/types/domain/user.py +12 -0
  60. {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.0.dist-info}/WHEEL +0 -0
  61. {maxapi_python-2.0.1.dist-info → maxapi_python-2.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -12,9 +12,9 @@ from pymax.api.response import (
12
12
  from pymax.exceptions import PyMaxError
13
13
  from pymax.logging import get_logger
14
14
  from pymax.protocol import Opcode
15
- from pymax.types.domain import Chat, Message
15
+ from pymax.types.domain import Chat, Member, Message
16
16
 
17
- from .enums import ChatLinkPrefix, ChatPayloadKey
17
+ from .enums import ChatLinkPrefix, ChatMemberOperation, ChatPayloadKey
18
18
  from .payloads import (
19
19
  ChangeGroupProfilePayload,
20
20
  ChangeGroupSettingsOptions,
@@ -23,9 +23,11 @@ from .payloads import (
23
23
  CreateGroupMessage,
24
24
  CreateGroupPayload,
25
25
  FetchChatsPayload,
26
+ FetchJoinRequests,
26
27
  GetChatInfoPayload,
27
28
  InviteUsersPayload,
28
29
  JoinChatPayload,
30
+ JoinRequestActionPayload,
29
31
  LeaveChatPayload,
30
32
  LinkInfoPayload,
31
33
  RemoveUsersPayload,
@@ -70,7 +72,9 @@ class ChatService:
70
72
  if self.app.chats is None:
71
73
  return
72
74
 
73
- self.app.chats = [chat for chat in self.app.chats if chat.id != chat_id]
75
+ self.app.chats = [
76
+ chat for chat in self.app.chats if chat.id != chat_id
77
+ ]
74
78
 
75
79
  @staticmethod
76
80
  def _process_chat_join_link(link: str) -> str | None:
@@ -108,7 +112,9 @@ class ChatService:
108
112
  return None
109
113
 
110
114
  chat = self._cache_chat(chat)
111
- message = require_payload_model(response, Message).bind(self.app.api.messages)
115
+ message = require_payload_model(response, Message).bind(
116
+ self.app.api.messages
117
+ )
112
118
  return chat, message
113
119
 
114
120
  async def invite_users_to_group(
@@ -139,7 +145,9 @@ class ChatService:
139
145
  user_ids: list[int],
140
146
  show_history: bool = True,
141
147
  ) -> Chat | None:
142
- return await self.invite_users_to_group(chat_id, user_ids, show_history)
148
+ return await self.invite_users_to_group(
149
+ chat_id, user_ids, show_history
150
+ )
143
151
 
144
152
  async def remove_users_from_group(
145
153
  self,
@@ -183,7 +191,9 @@ class ChatService:
183
191
  ),
184
192
  )
185
193
 
186
- response = await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload())
194
+ response = await self.app.invoke(
195
+ Opcode.CHAT_UPDATE, frame.to_payload()
196
+ )
187
197
  chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
188
198
  if chat:
189
199
  self._cache_chat(chat)
@@ -200,7 +210,9 @@ class ChatService:
200
210
  description=description,
201
211
  )
202
212
 
203
- response = await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload())
213
+ response = await self.app.invoke(
214
+ Opcode.CHAT_UPDATE, frame.to_payload()
215
+ )
204
216
  chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
205
217
  if chat:
206
218
  self._cache_chat(chat)
@@ -230,7 +242,9 @@ class ChatService:
230
242
 
231
243
  async def rework_invite_link(self, chat_id: int) -> Chat:
232
244
  frame = ReworkInviteLinkPayload(chat_id=chat_id)
233
- response = await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload())
245
+ response = await self.app.invoke(
246
+ Opcode.CHAT_UPDATE, frame.to_payload()
247
+ )
234
248
  chat = require_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
235
249
  return self._cache_chat(chat)
236
250
 
@@ -240,12 +254,18 @@ class ChatService:
240
254
  for chat_id in chat_ids
241
255
  if (chat := self._get_cached_chat(chat_id)) is not None
242
256
  }
243
- missed_chat_ids = [chat_id for chat_id in chat_ids if chat_id not in cached]
257
+ missed_chat_ids = [
258
+ chat_id for chat_id in chat_ids if chat_id not in cached
259
+ ]
244
260
 
245
261
  if missed_chat_ids:
246
262
  frame = GetChatInfoPayload(chat_ids=missed_chat_ids)
247
- response = await self.app.invoke(Opcode.CHAT_INFO, frame.to_payload())
248
- for chat in parse_payload_list(response, ChatPayloadKey.CHATS, Chat):
263
+ response = await self.app.invoke(
264
+ Opcode.CHAT_INFO, frame.to_payload()
265
+ )
266
+ for chat in parse_payload_list(
267
+ response, ChatPayloadKey.CHATS, Chat
268
+ ):
249
269
  chat = self._cache_chat(chat)
250
270
  cached[chat.id] = chat
251
271
 
@@ -272,6 +292,86 @@ class ChatService:
272
292
 
273
293
  chats = [
274
294
  self._cache_chat(chat)
275
- for chat in parse_payload_list(response, ChatPayloadKey.CHATS, Chat)
295
+ for chat in parse_payload_list(
296
+ response, ChatPayloadKey.CHATS, Chat
297
+ )
276
298
  ]
277
299
  return chats
300
+
301
+ async def get_join_requests(
302
+ self, chat_id: int, count: int = 100
303
+ ) -> list[Member]:
304
+ frame = FetchJoinRequests(chat_id=chat_id, count=count)
305
+
306
+ response = await self.app.invoke(
307
+ Opcode.CHAT_MEMBERS, frame.to_payload()
308
+ )
309
+
310
+ return parse_payload_list(response, ChatPayloadKey.MEMBERS, Member)
311
+
312
+ async def confirm_join_requests(
313
+ self,
314
+ chat_id: int,
315
+ user_ids: list[int],
316
+ show_history: bool = True,
317
+ ) -> Chat | None:
318
+ frame = JoinRequestActionPayload(
319
+ chat_id=chat_id,
320
+ user_ids=user_ids,
321
+ show_history=show_history,
322
+ operation=ChatMemberOperation.ADD,
323
+ )
324
+
325
+ response = await self.app.invoke(
326
+ Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload()
327
+ )
328
+
329
+ chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
330
+ if chat:
331
+ return self._cache_chat(chat)
332
+
333
+ return None
334
+
335
+ async def confirm_join_request(
336
+ self,
337
+ chat_id: int,
338
+ user_id: int,
339
+ show_history: bool = True,
340
+ ) -> Chat | None:
341
+ return await self.confirm_join_requests(
342
+ chat_id=chat_id,
343
+ user_ids=[user_id],
344
+ show_history=show_history,
345
+ )
346
+
347
+ async def decline_join_requests(
348
+ self,
349
+ chat_id: int,
350
+ user_ids: list[int],
351
+ ) -> Chat | None:
352
+ frame = JoinRequestActionPayload(
353
+ chat_id=chat_id,
354
+ user_ids=user_ids,
355
+ show_history=None,
356
+ operation=ChatMemberOperation.REMOVE,
357
+ )
358
+
359
+ response = await self.app.invoke(
360
+ Opcode.CHAT_MEMBERS_UPDATE, frame.to_payload()
361
+ )
362
+
363
+ chat = parse_payload_item_model(response, ChatPayloadKey.CHAT, Chat)
364
+ if chat:
365
+ return self._cache_chat(chat)
366
+
367
+ return None
368
+
369
+ async def decline_join_request(
370
+ self,
371
+ chat_id: int,
372
+ user_id: int,
373
+ ) -> Chat | None:
374
+ return await self.decline_join_requests(
375
+ chat_id=chat_id,
376
+ user_ids=[user_id],
377
+ )
pymax/api/facade.py CHANGED
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING
5
5
  from pymax.logging import get_logger
6
6
 
7
7
  from .auth import AuthService
8
+ from .bots import BotsService
8
9
  from .chats import ChatService
9
10
  from .messages import MessageService
10
11
  from .self import SelfService
@@ -25,6 +26,7 @@ class ApiFacade:
25
26
  self.messages = MessageService(app)
26
27
  self.chats = ChatService(app)
27
28
  self.users = UserService(app)
29
+ self.bots = BotsService(app)
28
30
  self.account = SelfService(app)
29
31
  self.session = SessionService(app)
30
32
  self.auth = AuthService(app)
@@ -3,7 +3,11 @@ from typing import Any
3
3
  from pydantic import Field
4
4
 
5
5
  from pymax.api.models import CamelModel
6
- from pymax.api.uploads.payloads import AttachFilePayload, AttachPhotoPayload, VideoAttachPayload
6
+ from pymax.api.uploads.payloads import (
7
+ AttachFilePayload,
8
+ AttachPhotoPayload,
9
+ VideoAttachPayload,
10
+ )
7
11
 
8
12
  from .enums import ItemType, ReadAction
9
13
 
@@ -9,7 +9,11 @@ from pymax.api.response import (
9
9
  payload_item,
10
10
  require_payload_model,
11
11
  )
12
- from pymax.api.uploads.payloads import AttachFilePayload, AttachPhotoPayload, VideoAttachPayload
12
+ from pymax.api.uploads.payloads import (
13
+ AttachFilePayload,
14
+ AttachPhotoPayload,
15
+ VideoAttachPayload,
16
+ )
13
17
  from pymax.exceptions import UploadError
14
18
  from pymax.files import File, Photo, Video
15
19
  from pymax.formatting.markdown import Formatter
@@ -65,13 +69,17 @@ class MessageService:
65
69
  async def _upload_attachments(
66
70
  self, attachments: SendAttachments
67
71
  ) -> list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload]:
68
- result: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload] = []
72
+ result: list[
73
+ AttachPhotoPayload | VideoAttachPayload | AttachFilePayload
74
+ ] = []
69
75
  if not attachments:
70
76
  return result
71
77
 
72
78
  for attachment in attachments:
73
79
  if isinstance(attachment, Photo):
74
- upload_result = await self.app.api.uploads.upload_photo(attachment)
80
+ upload_result = await self.app.api.uploads.upload_photo(
81
+ attachment
82
+ )
75
83
  if not upload_result:
76
84
  logger.error("Photo uploading failed")
77
85
  raise UploadError("Photo uploading failed")
@@ -79,7 +87,9 @@ class MessageService:
79
87
  result.append(upload_result)
80
88
 
81
89
  elif isinstance(attachment, Video):
82
- upload_result = await self.app.api.uploads.upload_video(attachment)
90
+ upload_result = await self.app.api.uploads.upload_video(
91
+ attachment
92
+ )
83
93
  if not upload_result:
84
94
  logger.error("Video uploading failed")
85
95
  raise UploadError("Video uploading failed")
@@ -87,7 +97,9 @@ class MessageService:
87
97
  result.append(upload_result)
88
98
 
89
99
  elif isinstance(attachment, File):
90
- upload_result = await self.app.api.uploads.upload_file(attachment)
100
+ upload_result = await self.app.api.uploads.upload_file(
101
+ attachment
102
+ )
91
103
  if not upload_result:
92
104
  logger.error("File uploading failed")
93
105
  raise UploadError("File uploading failed")
@@ -105,7 +117,9 @@ class MessageService:
105
117
  *,
106
118
  notify: bool = True,
107
119
  ) -> Message | None:
108
- logger.info("sending message chat_id=%s text_len=%s", chat_id, len(text))
120
+ logger.info(
121
+ "sending message chat_id=%s text_len=%s", chat_id, len(text)
122
+ )
109
123
 
110
124
  clean_text, elements = Formatter.format_markdown(text)
111
125
 
@@ -157,7 +171,10 @@ class MessageService:
157
171
  Opcode.CHAT_HISTORY,
158
172
  payload=frame.to_payload(),
159
173
  )
160
- return parse_payload_list(response, MessagePayloadKey.MESSAGES, Message) or None
174
+ return (
175
+ parse_payload_list(response, MessagePayloadKey.MESSAGES, Message)
176
+ or None
177
+ )
161
178
 
162
179
  async def delete_message(
163
180
  self,
@@ -178,7 +195,9 @@ class MessageService:
178
195
  )
179
196
 
180
197
  await self.app.invoke(Opcode.MSG_DELETE, frame.to_payload())
181
- logger.info("messages deleted chat_id=%s count=%s", chat_id, len(message_ids))
198
+ logger.info(
199
+ "messages deleted chat_id=%s count=%s", chat_id, len(message_ids)
200
+ )
182
201
  return True
183
202
 
184
203
  async def pin_message(
@@ -200,7 +219,9 @@ class MessageService:
200
219
  )
201
220
 
202
221
  await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload())
203
- logger.info("message pinned chat_id=%s message_id=%s", chat_id, message_id)
222
+ logger.info(
223
+ "message pinned chat_id=%s message_id=%s", chat_id, message_id
224
+ )
204
225
  return True
205
226
 
206
227
  async def get_video_by_id(
@@ -242,7 +263,9 @@ class MessageService:
242
263
  file_id=file_id,
243
264
  )
244
265
 
245
- response = await self.app.invoke(Opcode.FILE_DOWNLOAD, frame.to_payload())
266
+ response = await self.app.invoke(
267
+ Opcode.FILE_DOWNLOAD, frame.to_payload()
268
+ )
246
269
  return parse_payload_model(response, FileRequest)
247
270
 
248
271
  async def add_reaction(
@@ -263,7 +286,9 @@ class MessageService:
263
286
  reaction=ReactionInfoPayload(id=reaction),
264
287
  )
265
288
 
266
- response = await self.app.invoke(Opcode.MSG_REACTION, frame.to_payload())
289
+ response = await self.app.invoke(
290
+ Opcode.MSG_REACTION, frame.to_payload()
291
+ )
267
292
  reaction_info = payload_item(response, MessagePayloadKey.REACTION_INFO)
268
293
  if reaction_info:
269
294
  return ReactionInfo.model_validate(reaction_info)
pymax/api/self/service.py CHANGED
@@ -37,7 +37,9 @@ class SelfService:
37
37
  async def request_profile_photo_upload_url(self) -> str:
38
38
  logger.info("requesting profile photo upload url")
39
39
  frame = UploadPayload(profile=True)
40
- response = await self.app.invoke(Opcode.PHOTO_UPLOAD, frame.to_payload())
40
+ response = await self.app.invoke(
41
+ Opcode.PHOTO_UPLOAD, frame.to_payload()
42
+ )
41
43
  return str(require_payload_item(response, SelfPayloadKey.URL))
42
44
 
43
45
  async def change_profile(
@@ -84,13 +86,17 @@ class SelfService:
84
86
  include=chat_include,
85
87
  filters=filters or [],
86
88
  )
87
- response = await self.app.invoke(Opcode.FOLDERS_UPDATE, frame.to_payload())
89
+ response = await self.app.invoke(
90
+ Opcode.FOLDERS_UPDATE, frame.to_payload()
91
+ )
88
92
  return require_payload_model(response, FolderUpdate)
89
93
 
90
94
  async def get_folders(self, folder_sync: int = 0) -> FolderList:
91
95
  logger.info("fetching folders")
92
96
  frame = GetFolderPayload(folder_sync=folder_sync)
93
- response = await self.app.invoke(Opcode.FOLDERS_GET, frame.to_payload())
97
+ response = await self.app.invoke(
98
+ Opcode.FOLDERS_GET, frame.to_payload()
99
+ )
94
100
  return require_payload_model(response, FolderList)
95
101
 
96
102
  async def update_folder(
@@ -109,13 +115,17 @@ class SelfService:
109
115
  filters=filters or [],
110
116
  options=options or [],
111
117
  )
112
- response = await self.app.invoke(Opcode.FOLDERS_UPDATE, frame.to_payload())
118
+ response = await self.app.invoke(
119
+ Opcode.FOLDERS_UPDATE, frame.to_payload()
120
+ )
113
121
  return require_payload_model(response, FolderUpdate)
114
122
 
115
123
  async def delete_folder(self, folder_id: str) -> FolderUpdate:
116
124
  logger.info("deleting folder")
117
125
  frame = DeleteFolderPayload(folder_ids=[folder_id])
118
- response = await self.app.invoke(Opcode.FOLDERS_DELETE, frame.to_payload())
126
+ response = await self.app.invoke(
127
+ Opcode.FOLDERS_DELETE, frame.to_payload()
128
+ )
119
129
  return require_payload_model(response, FolderUpdate)
120
130
 
121
131
  async def close_all_sessions(self) -> bool:
@@ -129,7 +139,9 @@ class SelfService:
129
139
  token = payload_item(response, SelfPayloadKey.TOKEN, str)
130
140
 
131
141
  if not token:
132
- logger.warning("no token received after closing sessions, skipping token update")
142
+ logger.warning(
143
+ "no token received after closing sessions, skipping token update"
144
+ )
133
145
  return False
134
146
 
135
147
  await self.app.store.update_token(self.app.session.token, token)
@@ -52,10 +52,17 @@ class MobileUserAgentPayload(CamelModel):
52
52
  by_alias=True,
53
53
  exclude_none=True,
54
54
  )
55
- if self.device_type == DeviceType.WEB and "headerUserAgent" not in payload:
55
+ if (
56
+ self.device_type == DeviceType.WEB
57
+ and "headerUserAgent" not in payload
58
+ ):
56
59
  payload["headerUserAgent"] = DEFAULT_WEB_HEADER_USER_AGENT
57
60
 
58
- return {alias: payload[alias] for alias in WEB_USER_AGENT_ALIASES if alias in payload}
61
+ return {
62
+ alias: payload[alias]
63
+ for alias in WEB_USER_AGENT_ALIASES
64
+ if alias in payload
65
+ }
59
66
 
60
67
 
61
68
  class MobileHandshakePayload(CamelModel):
@@ -1,9 +1,6 @@
1
- from typing import Literal
2
-
3
- from pydantic import BaseModel, Field
1
+ from pydantic import BaseModel
4
2
 
5
3
  from pymax.api.models import CamelModel
6
- from pymax.types import AttachmentType
7
4
 
8
5
 
9
6
  class PhotoPayloadResponse(BaseModel):
@@ -5,18 +5,24 @@ from pymax.types import AttachmentType
5
5
 
6
6
 
7
7
  class AttachPhotoPayload(CamelModel):
8
- type: AttachmentType = Field(default=AttachmentType.PHOTO, serialization_alias="_type")
8
+ type: AttachmentType = Field(
9
+ default=AttachmentType.PHOTO, serialization_alias="_type"
10
+ )
9
11
  photo_token: str
10
12
 
11
13
 
12
14
  class VideoAttachPayload(CamelModel):
13
- type: AttachmentType = Field(default=AttachmentType.VIDEO, serialization_alias="_type")
15
+ type: AttachmentType = Field(
16
+ default=AttachmentType.VIDEO, serialization_alias="_type"
17
+ )
14
18
  video_id: int
15
19
  token: str
16
20
 
17
21
 
18
22
  class AttachFilePayload(CamelModel):
19
- type: AttachmentType = Field(default=AttachmentType.FILE, serialization_alias="_type")
23
+ type: AttachmentType = Field(
24
+ default=AttachmentType.FILE, serialization_alias="_type"
25
+ )
20
26
  file_id: int
21
27
 
22
28