maxapi-python 1.1.20__py3-none-any.whl → 1.2.1__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-1.1.20.dist-info → maxapi_python-1.2.1.dist-info}/METADATA +44 -54
- maxapi_python-1.2.1.dist-info/RECORD +32 -0
- pymax/core.py +79 -156
- pymax/crud.py +3 -7
- pymax/filters.py +158 -41
- pymax/formatter.py +1 -0
- pymax/formatting.py +4 -6
- pymax/interfaces.py +148 -8
- pymax/mixins/__init__.py +3 -0
- pymax/mixins/auth.py +229 -30
- pymax/mixins/channel.py +36 -37
- pymax/mixins/group.py +127 -8
- pymax/mixins/handler.py +163 -39
- pymax/mixins/message.py +251 -97
- pymax/mixins/scheduler.py +28 -0
- pymax/mixins/self.py +79 -40
- pymax/mixins/socket.py +254 -281
- pymax/mixins/user.py +63 -42
- pymax/mixins/websocket.py +145 -145
- pymax/payloads.py +12 -0
- pymax/static/constant.py +4 -2
- pymax/static/enum.py +5 -0
- maxapi_python-1.1.20.dist-info/RECORD +0 -31
- {maxapi_python-1.1.20.dist-info → maxapi_python-1.2.1.dist-info}/WHEEL +0 -0
- {maxapi_python-1.1.20.dist-info → maxapi_python-1.2.1.dist-info}/licenses/LICENSE +0 -0
pymax/mixins/user.py
CHANGED
|
@@ -15,13 +15,15 @@ from pymax.types import Contact, Session, User
|
|
|
15
15
|
class UserMixin(ClientProtocol):
|
|
16
16
|
def get_cached_user(self, user_id: int) -> User | None:
|
|
17
17
|
"""
|
|
18
|
-
Получает
|
|
18
|
+
Получает пользователя из кеша по его идентификатору.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
Проверяет внутренний кеш пользователей и возвращает объект User
|
|
21
|
+
если пользователь был ранее загружен.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
:param user_id: Идентификатор пользователя.
|
|
24
|
+
:type user_id: int
|
|
25
|
+
:return: Объект User из кеша или None, если пользователь не найден.
|
|
26
|
+
:rtype: User | None
|
|
25
27
|
"""
|
|
26
28
|
user = self._users.get(user_id)
|
|
27
29
|
self.logger.debug("get_cached_user id=%s hit=%s", user_id, bool(user))
|
|
@@ -29,7 +31,16 @@ class UserMixin(ClientProtocol):
|
|
|
29
31
|
|
|
30
32
|
async def get_users(self, user_ids: list[int]) -> list[User]:
|
|
31
33
|
"""
|
|
32
|
-
Получает информацию о пользователях по их
|
|
34
|
+
Получает информацию о пользователях по их идентификаторам.
|
|
35
|
+
|
|
36
|
+
Метод использует внутренний кеш для избежания повторных запросов.
|
|
37
|
+
Если пользователь уже загружен, берется из кеша, иначе выполняется
|
|
38
|
+
сетевой запрос к серверу.
|
|
39
|
+
|
|
40
|
+
:param user_ids: Список идентификаторов пользователей.
|
|
41
|
+
:type user_ids: list[int]
|
|
42
|
+
:return: Список объектов User в порядке, соответствующем входному списку.
|
|
43
|
+
:rtype: list[User]
|
|
33
44
|
"""
|
|
34
45
|
self.logger.debug("get_users ids=%s", user_ids)
|
|
35
46
|
cached = {uid: self._users[uid] for uid in user_ids if uid in self._users}
|
|
@@ -49,7 +60,15 @@ class UserMixin(ClientProtocol):
|
|
|
49
60
|
|
|
50
61
|
async def get_user(self, user_id: int) -> User | None:
|
|
51
62
|
"""
|
|
52
|
-
Получает информацию о пользователе по его
|
|
63
|
+
Получает информацию о пользователе по его идентификатору.
|
|
64
|
+
|
|
65
|
+
Метод использует внутренний кеш. Если пользователь уже загружен,
|
|
66
|
+
возвращает его из кеша, иначе выполняет запрос к серверу.
|
|
67
|
+
|
|
68
|
+
:param user_id: Идентификатор пользователя.
|
|
69
|
+
:type user_id: int
|
|
70
|
+
:return: Объект User или None, если пользователь не найден.
|
|
71
|
+
:rtype: User | None
|
|
53
72
|
"""
|
|
54
73
|
self.logger.debug("get_user id=%s", user_id)
|
|
55
74
|
if user_id in self._users:
|
|
@@ -63,7 +82,15 @@ class UserMixin(ClientProtocol):
|
|
|
63
82
|
|
|
64
83
|
async def fetch_users(self, user_ids: list[int]) -> list[User]:
|
|
65
84
|
"""
|
|
66
|
-
|
|
85
|
+
Загружает информацию о пользователях с сервера.
|
|
86
|
+
|
|
87
|
+
Запрашивает данные о пользователях по их идентификаторам и добавляет
|
|
88
|
+
их в внутренний кеш.
|
|
89
|
+
|
|
90
|
+
:param user_ids: Список идентификаторов пользователей для загрузки.
|
|
91
|
+
:type user_ids: list[int]
|
|
92
|
+
:return: Список загруженных объектов User.
|
|
93
|
+
:rtype: list[User]
|
|
67
94
|
"""
|
|
68
95
|
self.logger.info("Fetching users count=%d", len(user_ids))
|
|
69
96
|
|
|
@@ -83,13 +110,13 @@ class UserMixin(ClientProtocol):
|
|
|
83
110
|
|
|
84
111
|
async def search_by_phone(self, phone: str) -> User:
|
|
85
112
|
"""
|
|
86
|
-
|
|
113
|
+
Выполняет поиск пользователя по номеру телефона.
|
|
87
114
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
115
|
+
:param phone: Номер телефона пользователя.
|
|
116
|
+
:type phone: str
|
|
117
|
+
:return: Объект User с найденными данными пользователя.
|
|
118
|
+
:rtype: User
|
|
119
|
+
:raises Error: Если пользователь не найден или произошла ошибка.
|
|
93
120
|
"""
|
|
94
121
|
self.logger.info("Searching user by phone: %s", phone)
|
|
95
122
|
|
|
@@ -115,10 +142,13 @@ class UserMixin(ClientProtocol):
|
|
|
115
142
|
|
|
116
143
|
async def get_sessions(self) -> list[Session]:
|
|
117
144
|
"""
|
|
118
|
-
Получает информацию о
|
|
145
|
+
Получает информацию о всех активных сессиях пользователя.
|
|
146
|
+
|
|
147
|
+
Возвращает список всех сессий, в которых авторизован пользователь.
|
|
119
148
|
|
|
120
|
-
|
|
121
|
-
|
|
149
|
+
:return: Список объектов Session.
|
|
150
|
+
:rtype: list[Session]
|
|
151
|
+
:raises Error: Если произошла ошибка при получении данных.
|
|
122
152
|
"""
|
|
123
153
|
self.logger.info("Fetching sessions")
|
|
124
154
|
|
|
@@ -133,15 +163,6 @@ class UserMixin(ClientProtocol):
|
|
|
133
163
|
return [Session.from_dict(s) for s in data["payload"].get("sessions", [])]
|
|
134
164
|
|
|
135
165
|
async def _contact_action(self, payload: ContactActionPayload) -> dict[str, Any]:
|
|
136
|
-
"""
|
|
137
|
-
Действия с контактом
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
payload (ContactActionPayload): Полезная нагрузка
|
|
141
|
-
|
|
142
|
-
Return:
|
|
143
|
-
Полезная нагрузка ответа
|
|
144
|
-
"""
|
|
145
166
|
data = await self._send_and_wait(
|
|
146
167
|
opcode=Opcode.CONTACT_UPDATE, # 34
|
|
147
168
|
payload=payload.model_dump(by_alias=True),
|
|
@@ -157,11 +178,11 @@ class UserMixin(ClientProtocol):
|
|
|
157
178
|
"""
|
|
158
179
|
Добавляет контакт в список контактов
|
|
159
180
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
181
|
+
:param contact_id: ID контакта
|
|
182
|
+
:type contact_id: int
|
|
183
|
+
:return: Объект контакта
|
|
184
|
+
:rtype: Contact
|
|
185
|
+
:raises ResponseStructureError: Если структура ответа неверна
|
|
165
186
|
"""
|
|
166
187
|
payload = await self._contact_action(
|
|
167
188
|
ContactActionPayload(contact_id=contact_id, action=ContactAction.ADD)
|
|
@@ -175,11 +196,11 @@ class UserMixin(ClientProtocol):
|
|
|
175
196
|
"""
|
|
176
197
|
Удаляет контакт из списка контактов
|
|
177
198
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
199
|
+
:param contact_id: ID контакта
|
|
200
|
+
:type contact_id: int
|
|
201
|
+
:return: True если успешно
|
|
202
|
+
:rtype: Literal[True]
|
|
203
|
+
:raises ResponseStructureError: Если структура ответа неверна
|
|
183
204
|
"""
|
|
184
205
|
await self._contact_action(
|
|
185
206
|
ContactActionPayload(contact_id=contact_id, action=ContactAction.REMOVE)
|
|
@@ -190,11 +211,11 @@ class UserMixin(ClientProtocol):
|
|
|
190
211
|
"""
|
|
191
212
|
Получение айди лс (диалога)
|
|
192
213
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
214
|
+
:param first_user_id: ID первого пользователя
|
|
215
|
+
:type first_user_id: int
|
|
216
|
+
:param second_user_id: ID второго пользователя
|
|
217
|
+
:type second_user_id: int
|
|
218
|
+
:return: Айди диалога
|
|
219
|
+
:rtype: int
|
|
199
220
|
"""
|
|
200
221
|
return first_user_id ^ second_user_id
|
pymax/mixins/websocket.py
CHANGED
|
@@ -8,7 +8,7 @@ import websockets
|
|
|
8
8
|
from typing_extensions import override
|
|
9
9
|
|
|
10
10
|
from pymax.exceptions import LoginError, WebSocketNotConnectedError
|
|
11
|
-
from pymax.filters import
|
|
11
|
+
from pymax.filters import BaseFilter
|
|
12
12
|
from pymax.interfaces import ClientProtocol
|
|
13
13
|
from pymax.mixins.utils import MixinsUtils
|
|
14
14
|
from pymax.payloads import BaseWebSocketMessage, SyncPayload, UserAgentPayload
|
|
@@ -73,6 +73,11 @@ class WebSocketMixin(ClientProtocol):
|
|
|
73
73
|
) -> dict[str, Any] | None:
|
|
74
74
|
"""
|
|
75
75
|
Устанавливает соединение WebSocket с сервером и выполняет handshake.
|
|
76
|
+
|
|
77
|
+
:param user_agent: Пользовательский агент для handshake. Если None, используется значение по умолчанию.
|
|
78
|
+
:type user_agent: UserAgentPayload | None
|
|
79
|
+
:return: Результат handshake.
|
|
80
|
+
:rtype: dict[str, Any] | None
|
|
76
81
|
"""
|
|
77
82
|
if user_agent is None:
|
|
78
83
|
user_agent = UserAgentPayload()
|
|
@@ -101,14 +106,13 @@ class WebSocketMixin(ClientProtocol):
|
|
|
101
106
|
async def _handshake(self, user_agent: UserAgentPayload) -> dict[str, Any]:
|
|
102
107
|
self.logger.debug(
|
|
103
108
|
"Sending handshake with user_agent keys=%s",
|
|
104
|
-
user_agent.model_dump().keys(),
|
|
109
|
+
user_agent.model_dump(by_alias=True).keys(),
|
|
105
110
|
)
|
|
111
|
+
|
|
112
|
+
user_agent_json = user_agent.model_dump(by_alias=True)
|
|
106
113
|
resp = await self._send_and_wait(
|
|
107
114
|
opcode=Opcode.SESSION_INIT,
|
|
108
|
-
payload={
|
|
109
|
-
"deviceId": str(self._device_id),
|
|
110
|
-
"userAgent": user_agent, # TODO: вынести в статик мб
|
|
111
|
-
},
|
|
115
|
+
payload={"deviceId": str(self._device_id), "userAgent": user_agent_json},
|
|
112
116
|
)
|
|
113
117
|
|
|
114
118
|
if resp.get("payload", {}).get("error"):
|
|
@@ -120,12 +124,12 @@ class WebSocketMixin(ClientProtocol):
|
|
|
120
124
|
async def _process_message_handler(
|
|
121
125
|
self,
|
|
122
126
|
handler: Callable[[Message], Any],
|
|
123
|
-
filter:
|
|
127
|
+
filter: BaseFilter[Message] | None,
|
|
124
128
|
message: Message,
|
|
125
129
|
):
|
|
126
130
|
result = None
|
|
127
131
|
if filter:
|
|
128
|
-
if filter
|
|
132
|
+
if filter(message):
|
|
129
133
|
result = handler(message)
|
|
130
134
|
else:
|
|
131
135
|
return
|
|
@@ -134,6 +138,126 @@ class WebSocketMixin(ClientProtocol):
|
|
|
134
138
|
if asyncio.iscoroutine(result):
|
|
135
139
|
self._create_safe_task(result, name=f"handler-{handler.__name__}")
|
|
136
140
|
|
|
141
|
+
def _parse_json(self, raw: Any) -> dict[str, Any] | None:
|
|
142
|
+
try:
|
|
143
|
+
return json.loads(raw)
|
|
144
|
+
except Exception:
|
|
145
|
+
self.logger.warning("JSON parse error", exc_info=True)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def _handle_pending(self, seq: int | None, data: dict) -> bool:
|
|
149
|
+
if isinstance(seq, int):
|
|
150
|
+
fut = self._pending.get(seq)
|
|
151
|
+
if fut and not fut.done():
|
|
152
|
+
fut.set_result(data)
|
|
153
|
+
self.logger.debug("Matched response for pending seq=%s", seq)
|
|
154
|
+
return True
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
async def _handle_incoming_queue(self, data: dict[str, Any]) -> None:
|
|
158
|
+
if self._incoming:
|
|
159
|
+
try:
|
|
160
|
+
self._incoming.put_nowait(data)
|
|
161
|
+
except asyncio.QueueFull:
|
|
162
|
+
self.logger.warning(
|
|
163
|
+
"Incoming queue full; dropping message seq=%s", data.get("seq")
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
async def _handle_file_upload(self, data: dict[str, Any]) -> None:
|
|
167
|
+
if data.get("opcode") != Opcode.NOTIF_ATTACH:
|
|
168
|
+
return
|
|
169
|
+
payload = data.get("payload", {})
|
|
170
|
+
for key in ("fileId", "videoId"):
|
|
171
|
+
id_ = payload.get(key)
|
|
172
|
+
if id_ is not None:
|
|
173
|
+
fut = self._file_upload_waiters.pop(id_, None)
|
|
174
|
+
if fut and not fut.done():
|
|
175
|
+
fut.set_result(data)
|
|
176
|
+
self.logger.debug(
|
|
177
|
+
"Fulfilled file upload waiter for %s=%s", key, id_
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async def _handle_message_notifications(self, data: dict) -> None:
|
|
181
|
+
if data.get("opcode") != Opcode.NOTIF_MESSAGE.value:
|
|
182
|
+
return
|
|
183
|
+
payload = data.get("payload", {})
|
|
184
|
+
msg = Message.from_dict(payload)
|
|
185
|
+
if not msg:
|
|
186
|
+
return
|
|
187
|
+
handlers_map = {
|
|
188
|
+
MessageStatus.EDITED: self._on_message_edit_handlers,
|
|
189
|
+
MessageStatus.REMOVED: self._on_message_delete_handlers,
|
|
190
|
+
}
|
|
191
|
+
if msg.status and msg.status in handlers_map:
|
|
192
|
+
for handler, filter in handlers_map[msg.status]:
|
|
193
|
+
await self._process_message_handler(handler, filter, msg)
|
|
194
|
+
if msg.status is None:
|
|
195
|
+
for handler, filter in self._on_message_handlers:
|
|
196
|
+
await self._process_message_handler(handler, filter, msg)
|
|
197
|
+
|
|
198
|
+
async def _handle_reactions(self, data: dict):
|
|
199
|
+
if data.get("opcode") != Opcode.NOTIF_MSG_REACTIONS_CHANGED:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
payload = data.get("payload", {})
|
|
203
|
+
chat_id = payload.get("chatId")
|
|
204
|
+
message_id = payload.get("messageId")
|
|
205
|
+
|
|
206
|
+
if not (chat_id and message_id):
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
total_count = payload.get("totalCount")
|
|
210
|
+
your_reaction = payload.get("yourReaction")
|
|
211
|
+
counters = [ReactionCounter.from_dict(c) for c in payload.get("counters", [])]
|
|
212
|
+
|
|
213
|
+
reaction_info = ReactionInfo(
|
|
214
|
+
total_count=total_count,
|
|
215
|
+
your_reaction=your_reaction,
|
|
216
|
+
counters=counters,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
for handler in self._on_reaction_change_handlers:
|
|
220
|
+
try:
|
|
221
|
+
result = handler(message_id, chat_id, reaction_info)
|
|
222
|
+
if asyncio.iscoroutine(result):
|
|
223
|
+
await result
|
|
224
|
+
except Exception as e:
|
|
225
|
+
self.logger.exception("Error in on_reaction_change_handler: %s", e)
|
|
226
|
+
|
|
227
|
+
async def _handle_chat_updates(self, data: dict) -> None:
|
|
228
|
+
if data.get("opcode") != Opcode.NOTIF_CHAT:
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
payload = data.get("payload", {})
|
|
232
|
+
chat_data = payload.get("chat", {})
|
|
233
|
+
chat = Chat.from_dict(chat_data)
|
|
234
|
+
if not chat:
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
for handler in self._on_chat_update_handlers:
|
|
238
|
+
try:
|
|
239
|
+
result = handler(chat)
|
|
240
|
+
if asyncio.iscoroutine(result):
|
|
241
|
+
await result
|
|
242
|
+
except Exception as e:
|
|
243
|
+
self.logger.exception("Error in on_chat_update_handler: %s", e)
|
|
244
|
+
|
|
245
|
+
async def _handle_raw_receive(self, data: dict[str, Any]) -> None:
|
|
246
|
+
for handler in self._on_raw_receive_handlers:
|
|
247
|
+
try:
|
|
248
|
+
result = handler(data)
|
|
249
|
+
if asyncio.iscoroutine(result):
|
|
250
|
+
await result
|
|
251
|
+
except Exception as e:
|
|
252
|
+
self.logger.exception("Error in on_raw_receive_handler: %s", e)
|
|
253
|
+
|
|
254
|
+
async def _dispatch_incoming(self, data: dict[str, Any]) -> None:
|
|
255
|
+
await self._handle_raw_receive(data)
|
|
256
|
+
await self._handle_file_upload(data)
|
|
257
|
+
await self._handle_message_notifications(data)
|
|
258
|
+
await self._handle_reactions(data)
|
|
259
|
+
await self._handle_chat_updates(data)
|
|
260
|
+
|
|
137
261
|
async def _recv_loop(self) -> None:
|
|
138
262
|
if self._ws is None:
|
|
139
263
|
self.logger.warning("Recv loop started without websocket instance")
|
|
@@ -143,145 +267,22 @@ class WebSocketMixin(ClientProtocol):
|
|
|
143
267
|
while True:
|
|
144
268
|
try:
|
|
145
269
|
raw = await self._ws.recv()
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
self.logger.warning("JSON parse error", exc_info=True)
|
|
270
|
+
data = self._parse_json(raw)
|
|
271
|
+
|
|
272
|
+
if data is None:
|
|
150
273
|
continue
|
|
274
|
+
|
|
151
275
|
seq = data.get("seq")
|
|
152
|
-
|
|
276
|
+
if self._handle_pending(seq, data):
|
|
277
|
+
continue
|
|
153
278
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
except asyncio.QueueFull:
|
|
162
|
-
self.logger.warning(
|
|
163
|
-
"Incoming queue full; dropping message seq=%s",
|
|
164
|
-
data.get("seq"),
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
try: # TODO: переделать, временное решение
|
|
168
|
-
if data.get("opcode") == Opcode.NOTIF_ATTACH:
|
|
169
|
-
file_id = data.get("payload", {}).get("fileId", None)
|
|
170
|
-
video_id = data.get("payload", {}).get("videoId", None)
|
|
171
|
-
if file_id is not None:
|
|
172
|
-
fut = self._file_upload_waiters.pop(file_id, None)
|
|
173
|
-
if fut and not fut.done():
|
|
174
|
-
fut.set_result(data)
|
|
175
|
-
self.logger.debug(
|
|
176
|
-
"Fulfilled file upload waiter for fileId=%s",
|
|
177
|
-
file_id,
|
|
178
|
-
)
|
|
179
|
-
elif video_id is not None:
|
|
180
|
-
fut = self._file_upload_waiters.pop(video_id, None)
|
|
181
|
-
if fut and not fut.done():
|
|
182
|
-
fut.set_result(data)
|
|
183
|
-
self.logger.debug(
|
|
184
|
-
"Fulfilled file upload waiter for videoId=%s",
|
|
185
|
-
video_id,
|
|
186
|
-
)
|
|
187
|
-
except Exception:
|
|
188
|
-
self.logger.exception("Error handling file upload notification")
|
|
189
|
-
|
|
190
|
-
if data.get("opcode") == Opcode.NOTIF_MESSAGE.value and (
|
|
191
|
-
self._on_message_handlers
|
|
192
|
-
or self._on_message_edit_handlers
|
|
193
|
-
or self._on_message_delete_handlers
|
|
194
|
-
):
|
|
195
|
-
try:
|
|
196
|
-
for handler, filter in self._on_message_handlers:
|
|
197
|
-
payload = data.get("payload", {})
|
|
198
|
-
msg = Message.from_dict(payload)
|
|
199
|
-
if msg:
|
|
200
|
-
if msg.status:
|
|
201
|
-
if msg.status == MessageStatus.EDITED:
|
|
202
|
-
for (
|
|
203
|
-
edit_handler,
|
|
204
|
-
edit_filter,
|
|
205
|
-
) in self._on_message_edit_handlers:
|
|
206
|
-
await self._process_message_handler(
|
|
207
|
-
edit_handler,
|
|
208
|
-
edit_filter,
|
|
209
|
-
msg,
|
|
210
|
-
)
|
|
211
|
-
elif msg.status == MessageStatus.REMOVED:
|
|
212
|
-
for (
|
|
213
|
-
remove_handler,
|
|
214
|
-
remove_filter,
|
|
215
|
-
) in self._on_message_delete_handlers:
|
|
216
|
-
await self._process_message_handler(
|
|
217
|
-
remove_handler,
|
|
218
|
-
remove_filter,
|
|
219
|
-
msg,
|
|
220
|
-
)
|
|
221
|
-
await self._process_message_handler(
|
|
222
|
-
handler, filter, msg
|
|
223
|
-
)
|
|
224
|
-
except Exception:
|
|
225
|
-
self.logger.exception("Error in on_message_handler")
|
|
226
|
-
|
|
227
|
-
if data.get("opcode") == Opcode.NOTIF_MSG_REACTIONS_CHANGED:
|
|
228
|
-
try:
|
|
229
|
-
for (
|
|
230
|
-
reaction_handler,
|
|
231
|
-
) in self._on_reaction_change_handlers:
|
|
232
|
-
payload = data.get("payload", {})
|
|
233
|
-
|
|
234
|
-
chat_id = payload.get("chatId")
|
|
235
|
-
message_id = payload.get("messageId")
|
|
236
|
-
|
|
237
|
-
total_count = payload.get("totalCount")
|
|
238
|
-
your_reaction = payload.get("yourReaction")
|
|
239
|
-
counters = [
|
|
240
|
-
ReactionCounter.from_dict(c)
|
|
241
|
-
for c in payload.get("counters", [])
|
|
242
|
-
]
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
chat_id
|
|
246
|
-
and message_id
|
|
247
|
-
and (
|
|
248
|
-
total_count is not None
|
|
249
|
-
or your_reaction
|
|
250
|
-
or counters
|
|
251
|
-
)
|
|
252
|
-
):
|
|
253
|
-
reaction_info = ReactionInfo(
|
|
254
|
-
total_count=total_count,
|
|
255
|
-
your_reaction=your_reaction,
|
|
256
|
-
counters=counters,
|
|
257
|
-
)
|
|
258
|
-
result = reaction_handler(
|
|
259
|
-
message_id, chat_id, reaction_info
|
|
260
|
-
)
|
|
261
|
-
if asyncio.iscoroutine(result):
|
|
262
|
-
await result
|
|
263
|
-
|
|
264
|
-
except Exception as e:
|
|
265
|
-
self.logger.exception(
|
|
266
|
-
"Error in on_reaction_change_handler: %s", e
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
if data.get("opcode") == Opcode.NOTIF_CHAT:
|
|
270
|
-
try:
|
|
271
|
-
for (chat_update_handler,) in self._on_chat_update_handlers:
|
|
272
|
-
payload = data.get("payload", {})
|
|
273
|
-
chat = Chat.from_dict(payload.get("chat", {}))
|
|
274
|
-
if chat:
|
|
275
|
-
result = chat_update_handler(chat)
|
|
276
|
-
if asyncio.iscoroutine(result):
|
|
277
|
-
await result
|
|
278
|
-
except Exception as e:
|
|
279
|
-
self.logger.exception(
|
|
280
|
-
"Error in on_chat_update_handler: %s", e
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
except websockets.exceptions.ConnectionClosed:
|
|
284
|
-
self.logger.info("WebSocket connection closed; exiting recv loop")
|
|
279
|
+
await self._handle_incoming_queue(data)
|
|
280
|
+
await self._dispatch_incoming(data)
|
|
281
|
+
|
|
282
|
+
except websockets.exceptions.ConnectionClosed as e:
|
|
283
|
+
self.logger.info(
|
|
284
|
+
f"WebSocket connection closed with error: {e.code}, {e.reason}; exiting recv loop"
|
|
285
|
+
)
|
|
285
286
|
for fut in self._pending.values():
|
|
286
287
|
if not fut.done():
|
|
287
288
|
fut.set_exception(WebSocketNotConnectedError)
|
|
@@ -303,7 +304,6 @@ class WebSocketMixin(ClientProtocol):
|
|
|
303
304
|
pass
|
|
304
305
|
except Exception as e:
|
|
305
306
|
self.logger.exception("Error retrieving task exception: %s", e)
|
|
306
|
-
pass
|
|
307
307
|
|
|
308
308
|
async def _queue_message(
|
|
309
309
|
self,
|
pymax/payloads.py
CHANGED
|
@@ -4,6 +4,8 @@ from pydantic import AliasChoices, BaseModel, Field
|
|
|
4
4
|
|
|
5
5
|
from pymax.static.constant import (
|
|
6
6
|
DEFAULT_APP_VERSION,
|
|
7
|
+
DEFAULT_BUILD_NUMBER,
|
|
8
|
+
DEFAULT_CLIENT_SESSION_ID,
|
|
7
9
|
DEFAULT_DEVICE_LOCALE,
|
|
8
10
|
DEFAULT_DEVICE_NAME,
|
|
9
11
|
DEFAULT_DEVICE_TYPE,
|
|
@@ -284,6 +286,8 @@ class UserAgentPayload(CamelModel):
|
|
|
284
286
|
app_version: str = Field(default=DEFAULT_APP_VERSION)
|
|
285
287
|
screen: str = Field(default=DEFAULT_SCREEN)
|
|
286
288
|
timezone: str = Field(default=DEFAULT_TIMEZONE)
|
|
289
|
+
client_session_id: int = Field(default=DEFAULT_CLIENT_SESSION_ID)
|
|
290
|
+
build_number: int = Field(default=DEFAULT_BUILD_NUMBER)
|
|
287
291
|
|
|
288
292
|
|
|
289
293
|
class ReworkInviteLinkPayload(CamelModel):
|
|
@@ -328,3 +332,11 @@ class UpdateFolderPayload(CamelModel):
|
|
|
328
332
|
|
|
329
333
|
class DeleteFolderPayload(CamelModel):
|
|
330
334
|
folder_ids: list[str]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class LeaveChatPayload(CamelModel):
|
|
338
|
+
chat_id: int
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class FetchChatsPayload(CamelModel):
|
|
342
|
+
marker: int
|
pymax/static/constant.py
CHANGED
|
@@ -8,17 +8,19 @@ WEBSOCKET_URI: Final[str] = "wss://ws-api.oneme.ru/websocket"
|
|
|
8
8
|
WEBSOCKET_ORIGIN: Final[Origin] = Origin("https://web.max.ru")
|
|
9
9
|
HOST: Final[str] = "api.oneme.ru"
|
|
10
10
|
PORT: Final[int] = 443
|
|
11
|
-
DEFAULT_TIMEOUT: Final[float] =
|
|
11
|
+
DEFAULT_TIMEOUT: Final[float] = 20.0
|
|
12
12
|
DEFAULT_DEVICE_TYPE: Final[str] = "WEB"
|
|
13
13
|
DEFAULT_LOCALE: Final[str] = "ru"
|
|
14
14
|
DEFAULT_DEVICE_LOCALE: Final[str] = "ru"
|
|
15
15
|
DEFAULT_DEVICE_NAME: Final[str] = "Chrome"
|
|
16
|
-
DEFAULT_APP_VERSION: Final[str] = "25.
|
|
16
|
+
DEFAULT_APP_VERSION: Final[str] = "25.12.13"
|
|
17
17
|
DEFAULT_SCREEN: Final[str] = "1080x1920 1.0x"
|
|
18
18
|
DEFAULT_OS_VERSION: Final[str] = "Linux"
|
|
19
19
|
DEFAULT_USER_AGENT: Final[str] = (
|
|
20
20
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
|
|
21
21
|
)
|
|
22
|
+
DEFAULT_BUILD_NUMBER: Final[int] = 0x97CB
|
|
23
|
+
DEFAULT_CLIENT_SESSION_ID: Final[int] = 14
|
|
22
24
|
DEFAULT_TIMEZONE: Final[str] = "Europe/Moscow"
|
|
23
25
|
DEFAULT_CHAT_MEMBERS_LIMIT: Final[int] = 50
|
|
24
26
|
DEFAULT_MARKER_VALUE: Final[int] = 0
|
pymax/static/enum.py
CHANGED
|
@@ -141,6 +141,10 @@ class Opcode(int, Enum):
|
|
|
141
141
|
FOLDERS_DELETE = 276
|
|
142
142
|
NOTIF_FOLDERS = 277
|
|
143
143
|
|
|
144
|
+
GET_QR = 288
|
|
145
|
+
GET_QR_STATUS = 289
|
|
146
|
+
LOGIN_BY_QR = 291
|
|
147
|
+
|
|
144
148
|
|
|
145
149
|
class ChatType(str, Enum):
|
|
146
150
|
DIALOG = "DIALOG"
|
|
@@ -170,6 +174,7 @@ class AuthType(str, Enum):
|
|
|
170
174
|
START_AUTH = "START_AUTH"
|
|
171
175
|
CHECK_CODE = "CHECK_CODE"
|
|
172
176
|
REGISTER = "REGISTER"
|
|
177
|
+
RESEND = "RESEND"
|
|
173
178
|
|
|
174
179
|
|
|
175
180
|
class AccessType(str, Enum):
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
|
|
2
|
-
pymax/core.py,sha256=LqX56a5BagUYl1vpB55Y1pLZQdMoC86t6mIQVlkVByo,17322
|
|
3
|
-
pymax/crud.py,sha256=wmJh8MPi3L_HbYp7MJP0eXfDcnjgfkLDa9rHAmXtkow,3219
|
|
4
|
-
pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
|
|
5
|
-
pymax/files.py,sha256=AvFIr34Desq2p4CNWXIngRqeyTBKMT98VmcnI-zvUU0,3462
|
|
6
|
-
pymax/filters.py,sha256=4hehzyQlyxBxLiEhtSH_KQdFHx8gYaqyIOOD72EsSS0,1536
|
|
7
|
-
pymax/formatter.py,sha256=OVsTwambHhXlOMd0wVECJWuB_S2wSEVKdMLCzgPcvYQ,859
|
|
8
|
-
pymax/formatting.py,sha256=hhtmakfcQDzQRsAckPunnJOCKY2lFdLeIp8Yw53yY8s,2522
|
|
9
|
-
pymax/interfaces.py,sha256=Re8o5N7FSQ-5OgVlK4-WBltX27GheEbfFjoIYl9_u6I,3723
|
|
10
|
-
pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
|
|
11
|
-
pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
|
|
12
|
-
pymax/payloads.py,sha256=cEXY_cVL6SPyhoFTTZnn7dyUx9MMdtNT5SuQSQtL4rg,6983
|
|
13
|
-
pymax/types.py,sha256=_ARcVXLGHyiGAJKYPd6EU9QDKzz4VwS6kjTu3YEH_u4,35523
|
|
14
|
-
pymax/mixins/__init__.py,sha256=xvjcq-lFVHCPss_t8xxXya0OJnsh-owlBqtUlrXSCcw,695
|
|
15
|
-
pymax/mixins/auth.py,sha256=Emv-0WVB_orwv9L_V5gAHfp-VYVaVcbW6AlclW_K6W4,6731
|
|
16
|
-
pymax/mixins/channel.py,sha256=7c8GANyxZuNbIHNBVcPAmMa1qqA1IRf9cGPBS1oK_q4,5159
|
|
17
|
-
pymax/mixins/group.py,sha256=XWXNWluCvq4KkZWqv4sxLpzkXfH33U1yEP20_ZFtSM4,10624
|
|
18
|
-
pymax/mixins/handler.py,sha256=ZuYX8wSgNXJoSMArcwyHvY_bL9A7X0AXnAOz22ATA3k,5897
|
|
19
|
-
pymax/mixins/message.py,sha256=wYvkMPE9ORCSFd_9J-6ltf__4ELG_zaZ_Uey4rmCzHg,25460
|
|
20
|
-
pymax/mixins/self.py,sha256=3BdHfUyqw3dn3ctJX9_hilP1jOaTaunstZ7nH8Y_xcU,5436
|
|
21
|
-
pymax/mixins/socket.py,sha256=GEOscQKKC48bOAXXiDLoz8GusCLtjdxQnB5AK5xJbms,26997
|
|
22
|
-
pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
|
|
23
|
-
pymax/mixins/user.py,sha256=5utoK7Z-7lySOg0PEO69b6h_3kjfcnV-YydTZIdNj8g,7120
|
|
24
|
-
pymax/mixins/utils.py,sha256=s3FUf3i_wjn2Gbg5YY1rWZB-90ZEGrrcUuND_MqqSTE,853
|
|
25
|
-
pymax/mixins/websocket.py,sha256=toiXt9qxx6yTgnWJdEOeNfp414MD4zmbHp1qVgVkjnY,19850
|
|
26
|
-
pymax/static/constant.py,sha256=Q1NrmaRj17Gdhk3FmUp3HIwrad1TDorq3wFdQlOCzN8,1027
|
|
27
|
-
pymax/static/enum.py,sha256=ddw5SEVfRb2J9TXOa5IGhssNd-7RyKfwZBKx_UionEM,4562
|
|
28
|
-
maxapi_python-1.1.20.dist-info/METADATA,sha256=9yhRv1m8PbJJhnUdloacxlK0jAE0veq6zfXDP-Ok5nk,6245
|
|
29
|
-
maxapi_python-1.1.20.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
30
|
-
maxapi_python-1.1.20.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
|
|
31
|
-
maxapi_python-1.1.20.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|