maxapi-python 0.1.0__py3-none-any.whl → 0.1.2__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.
pymax/payloads.py ADDED
@@ -0,0 +1,175 @@
1
+ from typing import Any, Literal
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from pymax.static import AttachType, AuthType
6
+
7
+
8
+ def to_camel(string: str) -> str:
9
+ parts = string.split("_")
10
+ return parts[0] + "".join(word.capitalize() for word in parts[1:])
11
+
12
+
13
+ class CamelModel(BaseModel):
14
+ model_config = {
15
+ "alias_generator": to_camel,
16
+ "populate_by_name": True,
17
+ }
18
+
19
+
20
+ class BaseWebSocketMessage(BaseModel):
21
+ ver: int = 11
22
+ cmd: int
23
+ seq: int
24
+ opcode: int
25
+ payload: dict[str, Any]
26
+
27
+
28
+ class RequestCodePayload(CamelModel):
29
+ phone: str
30
+ type: AuthType = AuthType.START_AUTH
31
+ language: str = "ru"
32
+
33
+
34
+ class SendCodePayload(CamelModel):
35
+ token: str
36
+ verify_code: str
37
+ auth_token_type: AuthType = AuthType.CHECK_CODE
38
+
39
+
40
+ class SyncPayload(CamelModel):
41
+ interactive: bool = True
42
+ token: str
43
+ chats_sync: int = 0
44
+ contacts_sync: int = 0
45
+ presence_sync: int = 0
46
+ drafts_sync: int = 0
47
+ chats_count: int = 40
48
+
49
+
50
+ class ReplyLink(CamelModel):
51
+ type: str = "REPLY"
52
+ message_id: str
53
+
54
+
55
+ class UploadPhotoPayload(CamelModel):
56
+ count: int = 1
57
+
58
+
59
+ class AttachPhotoPayload(CamelModel):
60
+ type: AttachType = Field(AttachType.PHOTO, alias="_type")
61
+ photo_token: str
62
+
63
+
64
+ class SendMessagePayloadMessage(CamelModel):
65
+ text: str
66
+ cid: int
67
+ elements: list[Any]
68
+ attaches: list[dict[str, Any]]
69
+ link: ReplyLink | None = None
70
+
71
+
72
+ class SendMessagePayload(CamelModel):
73
+ chat_id: int
74
+ message: SendMessagePayloadMessage
75
+ notify: bool = False
76
+
77
+
78
+ class EditMessagePayload(CamelModel):
79
+ chat_id: int
80
+ message_id: int
81
+ text: str
82
+ elements: list[Any]
83
+ attaches: list[Any]
84
+
85
+
86
+ class DeleteMessagePayload(CamelModel):
87
+ chat_id: int
88
+ message_ids: list[int]
89
+ for_me: bool = False
90
+
91
+
92
+ class FetchContactsPayload(CamelModel):
93
+ contact_ids: list[int]
94
+
95
+
96
+ class FetchHistoryPayload(CamelModel):
97
+ chat_id: int
98
+ from_time: int = Field(alias="from")
99
+ forward: int
100
+ backward: int = 200
101
+ get_messages: bool = True
102
+
103
+
104
+ class ChangeProfilePayload(CamelModel):
105
+ first_name: str
106
+ last_name: str | None = None
107
+ description: str | None = None
108
+
109
+
110
+ class ResolveLinkPayload(CamelModel):
111
+ link: str
112
+
113
+
114
+ class PinMessagePayload(CamelModel):
115
+ chat_id: int
116
+ notify_pin: bool
117
+ pin_message_id: int
118
+
119
+
120
+ class CreateGroupAttach(CamelModel):
121
+ type: Literal["CONTROL"] = Field("CONTROL", alias="_type")
122
+ event: str = "new"
123
+ chat_type: str = "CHAT"
124
+ title: str
125
+ user_ids: list[int]
126
+
127
+
128
+ class CreateGroupMessage(CamelModel):
129
+ cid: int
130
+ attaches: list[CreateGroupAttach]
131
+
132
+
133
+ class CreateGroupPayload(CamelModel):
134
+ message: CreateGroupMessage
135
+ notify: bool = True
136
+
137
+
138
+ class InviteUsersPayload(CamelModel):
139
+ chat_id: int
140
+ user_ids: list[int]
141
+ show_history: bool
142
+ operation: str = "add"
143
+
144
+
145
+ class RemoveUsersPayload(CamelModel):
146
+ chat_id: int
147
+ user_ids: list[int]
148
+ operation: str = "remove"
149
+ clean_msg_period: int
150
+
151
+
152
+ class ChangeGroupSettingsOptions(BaseModel):
153
+ ONLY_OWNER_CAN_CHANGE_ICON_TITLE: bool | None
154
+ ALL_CAN_PIN_MESSAGE: bool | None
155
+ ONLY_ADMIN_CAN_ADD_MEMBER: bool | None
156
+ ONLY_ADMIN_CAN_CALL: bool | None
157
+ MEMBERS_CAN_SEE_PRIVATE_LINK: bool | None
158
+
159
+
160
+ class ChangeGroupSettingsPayload(CamelModel):
161
+ chat_id: int
162
+ options: ChangeGroupSettingsOptions
163
+
164
+
165
+ class ChangeGroupProfilePayload(CamelModel):
166
+ chat_id: int
167
+ theme: str | None
168
+ description: str | None
169
+
170
+
171
+ class GetGroupMembersPayload(CamelModel):
172
+ type: str = "MEMBER"
173
+ marker: int
174
+ chat_id: int
175
+ count: int
pymax/static.py CHANGED
@@ -3,27 +3,144 @@ from enum import Enum, IntEnum
3
3
 
4
4
  class Opcode(IntEnum):
5
5
  PING = 1
6
- STATS = 5
7
- HANDSHAKE = 6
6
+ DEBUG = 2
7
+ RECONNECT = 3
8
+ LOG = 5
9
+ SESSION_INIT = 6
8
10
  PROFILE = 16
9
- REQUEST_CODE = 17
10
- SEND_CODE = 18
11
- SYNC = 19
11
+ AUTH_REQUEST = 17
12
+ AUTH = 18
13
+ LOGIN = 19
14
+ LOGOUT = 20
15
+ SYNC = 21
16
+ CONFIG = 22
17
+ AUTH_CONFIRM = 23
18
+ PRESET_AVATARS = 25
19
+ ASSETS_GET = 26
12
20
  UNKNOWN_26 = 26
13
- SYNC_STICKERS_EMOJIS = 27
14
- GET_EMOJIS_BY_ID = 28
15
- GET_CONTACTS_INFO = 32
16
- GET_LAST_SEEN = 35
17
- GET_CHATS_DATA = 48
18
- FETCH_HISTORY = 49
19
-
20
- GET_HISTORY = 79
21
-
22
- SEND_MESSAGE = 64
23
- EDIT_MESSAGE = 67
24
- DELETE_MESSAGE = 68
25
-
26
- NEW_MESSAGE = 128
21
+ ASSETS_UPDATE = 27
22
+ ASSETS_GET_BY_IDS = 28
23
+ ASSETS_ADD = 29
24
+ SEARCH_FEEDBACK = 31
25
+ CONTACT_INFO = 32
26
+ CONTACT_ADD = 33
27
+ CONTACT_UPDATE = 34
28
+ CONTACT_PRESENCE = 35
29
+ CONTACT_LIST = 36
30
+ CONTACT_SEARCH = 37
31
+ CONTACT_MUTUAL = 38
32
+ CONTACT_PHOTOS = 39
33
+ CONTACT_SORT = 40
34
+ CONTACT_VERIFY = 42
35
+ REMOVE_CONTACT_PHOTO = 43
36
+ CONTACT_INFO_BY_PHONE = 46
37
+ CHAT_INFO = 48
38
+ CHAT_HISTORY = 49
39
+ CHAT_MARK = 50
40
+ CHAT_MEDIA = 51
41
+ CHAT_DELETE = 52
42
+ CHATS_LIST = 53
43
+ CHAT_CLEAR = 54
44
+ CHAT_UPDATE = 55
45
+ CHAT_CHECK_LINK = 56
46
+ CHAT_JOIN = 57
47
+ CHAT_LEAVE = 58
48
+ CHAT_MEMBERS = 59
49
+ PUBLIC_SEARCH = 60
50
+ CHAT_CLOSE = 61
51
+ CHAT_CREATE = 63
52
+ MSG_SEND = 64
53
+ MSG_TYPING = 65
54
+ MSG_DELETE = 66
55
+ MSG_EDIT = 67
56
+ CHAT_SEARCH = 68
57
+ MSG_SHARE_PREVIEW = 70
58
+ MSG_GET = 71
59
+ MSG_SEARCH_TOUCH = 72
60
+ MSG_SEARCH = 73
61
+ MSG_GET_STAT = 74
62
+ CHAT_SUBSCRIBE = 75
63
+ VIDEO_CHAT_START = 76
64
+ CHAT_MEMBERS_UPDATE = 77
65
+ VIDEO_CHAT_HISTORY = 79
66
+ PHOTO_UPLOAD = 80
67
+ STICKER_UPLOAD = 81
68
+ VIDEO_UPLOAD = 82
69
+ VIDEO_PLAY = 83
70
+ CHAT_PIN_SET_VISIBILITY = 86
71
+ FILE_UPLOAD = 87
72
+ FILE_DOWNLOAD = 88
73
+ LINK_INFO = 89
74
+ MSG_DELETE_RANGE = 92
75
+ SESSIONS_INFO = 96
76
+ SESSIONS_CLOSE = 97
77
+ PHONE_BIND_REQUEST = 98
78
+ PHONE_BIND_CONFIRM = 99
79
+ CONFIRM_PRESENT = 101
80
+ GET_INBOUND_CALLS = 103
81
+ EXTERNAL_CALLBACK = 105
82
+ AUTH_VALIDATE_PASSWORD = 107
83
+ AUTH_VALIDATE_HINT = 108
84
+ AUTH_VERIFY_EMAIL = 109
85
+ AUTH_CHECK_EMAIL = 110
86
+ AUTH_SET_2FA = 111
87
+ AUTH_CREATE_TRACK = 112
88
+ AUTH_LOGIN_CHECK_PASSWORD = 115
89
+ CHAT_COMPLAIN = 117
90
+ MSG_SEND_CALLBACK = 118
91
+ SUSPEND_BOT = 119
92
+ LOCATION_STOP = 124
93
+ LOCATION_SEND = 125
94
+ LOCATION_REQUEST = 126
95
+ GET_LAST_MENTIONS = 127
96
+ NOTIF_MESSAGE = 128
97
+ NOTIF_TYPING = 129
98
+ NOTIF_MARK = 130
99
+ NOTIF_CONTACT = 131
100
+ NOTIF_PRESENCE = 132
101
+ NOTIF_CONFIG = 134
102
+ NOTIF_CHAT = 135
103
+ NOTIF_ATTACH = 136
104
+ NOTIF_CALL_START = 137
105
+ NOTIF_CONTACT_SORT = 139
106
+ NOTIF_MSG_DELETE_RANGE = 140
107
+ NOTIF_MSG_DELETE = 142
108
+ NOTIF_CALLBACK_ANSWER = 143
109
+ CHAT_BOT_COMMANDS = 144
110
+ BOT_INFO = 145
111
+ NOTIF_LOCATION = 147
112
+ NOTIF_LOCATION_REQUEST = 148
113
+ NOTIF_ASSETS_UPDATE = 150
114
+ NOTIF_DRAFT = 152
115
+ NOTIF_DRAFT_DISCARD = 153
116
+ NOTIF_MSG_DELAYED = 154
117
+ NOTIF_MSG_REACTIONS_CHANGED = 155
118
+ NOTIF_MSG_YOU_REACTED = 156
119
+ CALLS_TOKEN = 158
120
+ NOTIF_PROFILE = 159
121
+ WEB_APP_INIT_DATA = 160
122
+ DRAFT_SAVE = 176
123
+ DRAFT_DISCARD = 177
124
+ MSG_REACTION = 178
125
+ MSG_CANCEL_REACTION = 179
126
+ MSG_GET_REACTIONS = 180
127
+ MSG_GET_DETAILED_REACTIONS = 181
128
+ STICKER_CREATE = 193
129
+ STICKER_SUGGEST = 194
130
+ VIDEO_CHAT_MEMBERS = 195
131
+ CHAT_HIDE = 196
132
+ CHAT_SEARCH_COMMON_PARTICIPANTS = 198
133
+ PROFILE_DELETE = 199
134
+ PROFILE_DELETE_TIME = 200
135
+ ASSETS_REMOVE = 259
136
+ ASSETS_MOVE = 260
137
+ ASSETS_LIST_MODIFY = 261
138
+ FOLDERS_GET = 272
139
+ FOLDERS_GET_BY_ID = 273
140
+ FOLDERS_UPDATE = 274
141
+ FOLDERS_REORDER = 275
142
+ FOLDERS_DELETE = 276
143
+ NOTIF_FOLDERS = 277
27
144
 
28
145
 
29
146
  class ChatType(str, Enum):
@@ -69,6 +186,13 @@ class DeviceType(str, Enum):
69
186
  IOS = "IOS"
70
187
 
71
188
 
189
+ class AttachType(str, Enum):
190
+ PHOTO = "PHOTO"
191
+ VIDEO = "VIDEO"
192
+ FILE = "FILE"
193
+ STICKER = "STICKER"
194
+
195
+
72
196
  class Constants(Enum):
73
197
  PHONE_REGEX = r"^\+?\d{10,15}$"
74
198
  WEBSOCKET_URI = "wss://ws-api.oneme.ru/websocket"
pymax/types.py CHANGED
@@ -1,10 +1,83 @@
1
- from typing import Any
1
+ from typing import Any, override
2
2
 
3
- from .static import AccessType, ChatType, ElementType, MessageStatus, MessageType
3
+ from .static import (
4
+ AccessType,
5
+ AttachType,
6
+ ChatType,
7
+ ElementType,
8
+ MessageStatus,
9
+ MessageType,
10
+ )
11
+
12
+
13
+ class Names:
14
+ def __init__(
15
+ self, name: str, first_name: str, last_name: str | None, type: str
16
+ ) -> None:
17
+ self.name = name
18
+ self.first_name = first_name
19
+ self.last_name = last_name
20
+ self.type = type
21
+
22
+ @classmethod
23
+ def from_dict(cls, data: dict[str, Any]) -> "Names":
24
+ return cls(
25
+ name=data["name"],
26
+ first_name=data["firstName"],
27
+ last_name=data.get("lastName"),
28
+ type=data["type"],
29
+ )
30
+
31
+ @override
32
+ def __repr__(self) -> str:
33
+ return f"Names(name={self.name!r}, first_name={self.first_name!r}, last_name={self.last_name!r}, type={self.type!r})"
34
+
35
+ @override
36
+ def __str__(self) -> str:
37
+ return self.name
38
+
39
+
40
+ class Me:
41
+ def __init__(
42
+ self,
43
+ id: int,
44
+ account_status: int,
45
+ phone: str,
46
+ names: list[Names],
47
+ update_time: int,
48
+ options: list[str] | None = None,
49
+ ) -> None:
50
+ self.id = id
51
+ self.account_status = account_status
52
+ self.phone = phone
53
+ self.update_time = update_time
54
+ self.options = options
55
+ self.names = names
56
+
57
+ @classmethod
58
+ def from_dict(cls, data: dict[str, Any]) -> "Me":
59
+ return cls(
60
+ id=data["id"],
61
+ account_status=data["accountStatus"],
62
+ phone=data["phone"],
63
+ names=[Names.from_dict(n) for n in data["names"]],
64
+ update_time=data["updateTime"],
65
+ options=data.get("options"),
66
+ )
67
+
68
+ @override
69
+ def __repr__(self) -> str:
70
+ return f"Me(id={self.id!r}, account_status={self.account_status!r}, phone={self.phone!r}, names={self.names!r}, update_time={self.update_time!r}, options={self.options!r})"
71
+
72
+ @override
73
+ def __str__(self) -> str:
74
+ return f"Me {self.id}: {', '.join(str(n) for n in self.names)}"
4
75
 
5
76
 
6
77
  class Element:
7
- def __init__(self, type: ElementType | str, length: int, from_: int | None = None) -> None:
78
+ def __init__(
79
+ self, type: ElementType | str, length: int, from_: int | None = None
80
+ ) -> None:
8
81
  self.type = type
9
82
  self.length = length
10
83
  self.from_ = from_
@@ -13,9 +86,13 @@ class Element:
13
86
  def from_dict(cls, data: dict[Any, Any]) -> "Element":
14
87
  return cls(type=data["type"], length=data["length"], from_=data.get("from"))
15
88
 
89
+ @override
16
90
  def __repr__(self) -> str:
17
- return f"Element(type={self.type!r}, length={self.length!r}, from_={self.from_!r})"
91
+ return (
92
+ f"Element(type={self.type!r}, length={self.length!r}, from_={self.from_!r})"
93
+ )
18
94
 
95
+ @override
19
96
  def __str__(self) -> str:
20
97
  return f"{self.type}({self.length})"
21
98
 
@@ -60,12 +137,14 @@ class Message:
60
137
  reaction_info=data.get("reactionInfo"),
61
138
  )
62
139
 
140
+ @override
63
141
  def __repr__(self) -> str:
64
142
  return (
65
143
  f"Message(id={self.id!r}, sender={self.sender!r}, text={self.text!r}, "
66
144
  f"type={self.type!r}, status={self.status!r}, elements={self.elements!r})"
67
145
  )
68
146
 
147
+ @override
69
148
  def __str__(self) -> str:
70
149
  return f"Message {self.id} from {self.sender}: {self.text}"
71
150
 
@@ -130,9 +209,11 @@ class Dialog:
130
209
  participants=data["participants"],
131
210
  )
132
211
 
212
+ @override
133
213
  def __repr__(self) -> str:
134
214
  return f"Dialog(id={self.id!r}, owner={self.owner!r}, type={self.type!r}, last_message={self.last_message!r})"
135
215
 
216
+ @override
136
217
  def __str__(self) -> str:
137
218
  return f"Dialog {self.id} ({self.type})"
138
219
 
@@ -199,10 +280,14 @@ class Chat:
199
280
  @classmethod
200
281
  def from_dict(cls, data: dict[Any, Any]) -> "Chat":
201
282
  raw_admins = data.get("adminParticipants", {}) or {}
202
- admin_participants: dict[int, dict[Any, Any]] = {int(k): v for k, v in raw_admins.items()}
283
+ admin_participants: dict[int, dict[Any, Any]] = {
284
+ int(k): v for k, v in raw_admins.items()
285
+ }
203
286
  raw_participants = data.get("participants", {}) or {}
204
287
  participants: dict[int, int] = {int(k): v for k, v in raw_participants.items()}
205
- last_msg = Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None
288
+ last_msg = (
289
+ Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None
290
+ )
206
291
  return cls(
207
292
  participants_count=data.get("participantsCount", 0),
208
293
  access=AccessType(data.get("access", AccessType.PUBLIC.value)),
@@ -233,44 +318,25 @@ class Chat:
233
318
  cid=data.get("cid", 0),
234
319
  )
235
320
 
321
+ @override
236
322
  def __repr__(self) -> str:
237
323
  return f"Chat(id={self.id!r}, title={self.title!r}, type={self.type!r})"
238
324
 
325
+ @override
239
326
  def __str__(self) -> str:
240
327
  return f"{self.title} ({self.type})"
241
328
 
242
329
 
243
330
  class Channel(Chat):
331
+ @override
244
332
  def __repr__(self) -> str:
245
333
  return f"Channel(id={self.id!r}, title={self.title!r})"
246
334
 
335
+ @override
247
336
  def __str__(self) -> str:
248
337
  return f"Channel: {self.title}"
249
338
 
250
339
 
251
- class Names:
252
- def __init__(self, name: str, first_name: str, last_name: str | None, type: str) -> None:
253
- self.name = name
254
- self.first_name = first_name
255
- self.last_name = last_name
256
- self.type = type
257
-
258
- @classmethod
259
- def from_dict(cls, data: dict[str, Any]) -> "Names":
260
- return cls(
261
- name=data["name"],
262
- first_name=data["firstName"],
263
- last_name=data.get("lastName"),
264
- type=data["type"],
265
- )
266
-
267
- def __repr__(self) -> str:
268
- return f"Names(name={self.name!r}, first_name={self.first_name!r}, last_name={self.last_name!r}, type={self.type!r})"
269
-
270
- def __str__(self) -> str:
271
- return self.name
272
-
273
-
274
340
  class User:
275
341
  def __init__(
276
342
  self,
@@ -320,8 +386,47 @@ class User:
320
386
  menu_button=data.get("menuButton"),
321
387
  )
322
388
 
389
+ @override
323
390
  def __repr__(self) -> str:
324
391
  return f"User(id={self.id!r}, names={self.names!r}, status={self.account_status!r})"
325
392
 
393
+ @override
326
394
  def __str__(self) -> str:
327
395
  return f"User {self.id}: {', '.join(str(n) for n in self.names)}"
396
+
397
+
398
+ class Attach:
399
+ def __init__(
400
+ self,
401
+ _type: AttachType,
402
+ video_id: int | None = None,
403
+ photo_token: str | None = None,
404
+ file_id: int | None = None,
405
+ token: str | None = None,
406
+ ) -> None:
407
+ self.type = _type
408
+ self.video_id = video_id
409
+ self.photo_token = photo_token
410
+ self.file_id = file_id
411
+ self.token = token
412
+
413
+ @classmethod
414
+ def from_dict(cls, data: dict[str, Any]) -> "Attach":
415
+ return cls(
416
+ _type=AttachType(data["type"]),
417
+ video_id=data.get("videoId"),
418
+ photo_token=data.get("photoToken"),
419
+ file_id=data.get("fileId"),
420
+ token=data.get("token"),
421
+ )
422
+
423
+ @override
424
+ def __repr__(self) -> str:
425
+ return (
426
+ f"Attach(type={self.type!r}, video_id={self.video_id!r}, "
427
+ f"photo_token={self.photo_token!r}, file_id={self.file_id!r}, token={self.token!r})"
428
+ )
429
+
430
+ @override
431
+ def __str__(self) -> str:
432
+ return f"Attach: {self.type}"
pymax/utils.py ADDED
@@ -0,0 +1,38 @@
1
+ from typing import Any
2
+
3
+ import lz4.block
4
+ import msgpack
5
+
6
+
7
+ def unpack_packet(data: bytes) -> None | dict[str, Any]:
8
+ ver = int.from_bytes(data[0:1], "big")
9
+ cmd = int.from_bytes(data[1:3], "big")
10
+ seq = int.from_bytes(data[3:4], "big")
11
+ opcode = int.from_bytes(data[4:6], "big")
12
+ packed_len = int.from_bytes(data[6:10], "big", signed=False)
13
+ comp_flag = packed_len >> 24
14
+ payload_length = packed_len & 0xFFFFFF
15
+ payload_bytes = data[10 : 10 + payload_length]
16
+ if comp_flag != 0:
17
+ compressed_data = payload_bytes
18
+ try:
19
+ payload_bytes = lz4.block.decompress(compressed_data, uncompressed_size=255)
20
+ except lz4.block.LZ4BlockError:
21
+ return None
22
+ payload = msgpack.unpackb(payload_bytes, raw=False)
23
+ return {"ver": ver, "cmd": cmd, "seq": seq, "opcode": opcode, "payload": payload}
24
+
25
+
26
+ # ToDo: Add lz4 compression
27
+ def pack_packet(
28
+ ver: int, cmd: int, seq: int, opcode: int, payload: dict[str, Any]
29
+ ) -> bytes:
30
+ ver_b = ver.to_bytes(1, "big")
31
+ cmd_b = cmd.to_bytes(2, "big")
32
+ seq_b = seq.to_bytes(1, "big")
33
+ opcode_b = opcode.to_bytes(2, "big")
34
+ payload_bytes = msgpack.packb(payload)
35
+ if payload_bytes is None:
36
+ payload_bytes = b""
37
+ payload_len_b = len(payload_bytes).to_bytes(4, "big")
38
+ return ver_b + cmd_b + seq_b + opcode_b + payload_len_b + payload_bytes