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/socket.py
CHANGED
|
@@ -11,7 +11,7 @@ import msgpack
|
|
|
11
11
|
from typing_extensions import override
|
|
12
12
|
|
|
13
13
|
from pymax.exceptions import Error, SocketNotConnectedError, SocketSendError
|
|
14
|
-
from pymax.filters import
|
|
14
|
+
from pymax.filters import BaseFilter
|
|
15
15
|
from pymax.interfaces import ClientProtocol
|
|
16
16
|
from pymax.payloads import BaseWebSocketMessage, SyncPayload, UserAgentPayload
|
|
17
17
|
from pymax.static.constant import (
|
|
@@ -92,7 +92,17 @@ class SocketMixin(ClientProtocol):
|
|
|
92
92
|
payload_len_b = payload_len.to_bytes(4, "big")
|
|
93
93
|
return ver_b + cmd_b + seq_b + opcode_b + payload_len_b + payload_bytes
|
|
94
94
|
|
|
95
|
-
async def connect(self, user_agent: UserAgentPayload) -> dict[str, Any]:
|
|
95
|
+
async def connect(self, user_agent: UserAgentPayload | None = None) -> dict[str, Any]:
|
|
96
|
+
"""
|
|
97
|
+
Устанавливает соединение с сервером и выполняет handshake.
|
|
98
|
+
|
|
99
|
+
:param user_agent: Пользовательский агент для handshake. Если None, используется значение по умолчанию.
|
|
100
|
+
:type user_agent: UserAgentPayload | None
|
|
101
|
+
:return: Результат handshake.
|
|
102
|
+
:rtype: dict[str, Any] | None
|
|
103
|
+
"""
|
|
104
|
+
if user_agent is None:
|
|
105
|
+
user_agent = UserAgentPayload()
|
|
96
106
|
if sys.version_info[:2] == (3, 12):
|
|
97
107
|
self.logger.warning(
|
|
98
108
|
"""
|
|
@@ -107,9 +117,7 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
107
117
|
raw_sock = await loop.run_in_executor(
|
|
108
118
|
None, lambda: socket.create_connection((self.host, self.port))
|
|
109
119
|
)
|
|
110
|
-
self._socket = self._ssl_context.wrap_socket(
|
|
111
|
-
raw_sock, server_hostname=self.host
|
|
112
|
-
)
|
|
120
|
+
self._socket = self._ssl_context.wrap_socket(raw_sock, server_hostname=self.host)
|
|
113
121
|
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
|
114
122
|
self.is_connected = True
|
|
115
123
|
self._incoming = asyncio.Queue()
|
|
@@ -139,6 +147,182 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
139
147
|
self.logger.error("Handshake failed: %s", e, exc_info=True)
|
|
140
148
|
raise ConnectionError(f"Handshake failed: {e}")
|
|
141
149
|
|
|
150
|
+
def _recv_exactly(self, sock: socket.socket, n: int) -> bytes:
|
|
151
|
+
buf = bytearray()
|
|
152
|
+
while len(buf) < n:
|
|
153
|
+
chunk = sock.recv(n - len(buf))
|
|
154
|
+
if not chunk:
|
|
155
|
+
return bytes(buf)
|
|
156
|
+
buf.extend(chunk)
|
|
157
|
+
return bytes(buf)
|
|
158
|
+
|
|
159
|
+
async def _parse_header(
|
|
160
|
+
self, loop: asyncio.AbstractEventLoop, sock: socket.socket
|
|
161
|
+
) -> bytes | None:
|
|
162
|
+
header = await loop.run_in_executor(None, lambda: self._recv_exactly(sock=sock, n=10))
|
|
163
|
+
if not header or len(header) < 10:
|
|
164
|
+
self.logger.info("Socket connection closed; exiting recv loop")
|
|
165
|
+
self.is_connected = False
|
|
166
|
+
try:
|
|
167
|
+
sock.close()
|
|
168
|
+
except Exception:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
return header
|
|
172
|
+
|
|
173
|
+
async def _recv_data(
|
|
174
|
+
self, loop: asyncio.AbstractEventLoop, header: bytes, sock: socket.socket
|
|
175
|
+
) -> list[dict[str, Any]] | None:
|
|
176
|
+
packed_len = int.from_bytes(header[6:10], "big", signed=False)
|
|
177
|
+
payload_length = packed_len & 0xFFFFFF
|
|
178
|
+
remaining = payload_length
|
|
179
|
+
payload = bytearray()
|
|
180
|
+
|
|
181
|
+
while remaining > 0:
|
|
182
|
+
min_read = min(remaining, 8192)
|
|
183
|
+
chunk = await loop.run_in_executor(None, lambda: self._recv_exactly(sock, min_read))
|
|
184
|
+
if not chunk:
|
|
185
|
+
self.logger.error("Connection closed while reading payload")
|
|
186
|
+
break
|
|
187
|
+
payload.extend(chunk)
|
|
188
|
+
remaining -= len(chunk)
|
|
189
|
+
|
|
190
|
+
if remaining > 0:
|
|
191
|
+
self.logger.error("Incomplete payload received; skipping packet")
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
raw = header + payload
|
|
195
|
+
if len(raw) < 10 + payload_length:
|
|
196
|
+
self.logger.error(
|
|
197
|
+
"Incomplete packet: expected %d bytes, got %d",
|
|
198
|
+
10 + payload_length,
|
|
199
|
+
len(raw),
|
|
200
|
+
)
|
|
201
|
+
await asyncio.sleep(RECV_LOOP_BACKOFF_DELAY)
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
data = self._unpack_packet(raw)
|
|
205
|
+
if not data:
|
|
206
|
+
self.logger.warning("Failed to unpack packet, skipping")
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
payload_objs = data.get("payload")
|
|
210
|
+
return (
|
|
211
|
+
[{**data, "payload": obj} for obj in payload_objs]
|
|
212
|
+
if isinstance(payload_objs, list)
|
|
213
|
+
else [data]
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _handle_pending(self, seq: int | None, data: dict) -> bool:
|
|
217
|
+
if isinstance(seq, int):
|
|
218
|
+
fut = self._pending.get(seq)
|
|
219
|
+
if fut and not fut.done():
|
|
220
|
+
fut.set_result(data)
|
|
221
|
+
self.logger.debug("Matched response for pending seq=%s", seq)
|
|
222
|
+
return True
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
async def _handle_incoming_queue(self, data: dict[str, Any]) -> None:
|
|
226
|
+
if self._incoming:
|
|
227
|
+
try:
|
|
228
|
+
self._incoming.put_nowait(data)
|
|
229
|
+
except asyncio.QueueFull:
|
|
230
|
+
self.logger.warning(
|
|
231
|
+
"Incoming queue full; dropping message seq=%s", data.get("seq")
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
async def _handle_file_upload(self, data: dict[str, Any]) -> None:
|
|
235
|
+
if data.get("opcode") != Opcode.NOTIF_ATTACH:
|
|
236
|
+
return
|
|
237
|
+
payload = data.get("payload", {})
|
|
238
|
+
for key in ("fileId", "videoId"):
|
|
239
|
+
id_ = payload.get(key)
|
|
240
|
+
if id_ is not None:
|
|
241
|
+
fut = self._file_upload_waiters.pop(id_, None)
|
|
242
|
+
if fut and not fut.done():
|
|
243
|
+
fut.set_result(data)
|
|
244
|
+
self.logger.debug("Fulfilled file upload waiter for %s=%s", key, id_)
|
|
245
|
+
|
|
246
|
+
async def _handle_message_notifications(self, data: dict) -> None:
|
|
247
|
+
if data.get("opcode") != Opcode.NOTIF_MESSAGE.value:
|
|
248
|
+
return
|
|
249
|
+
payload = data.get("payload", {})
|
|
250
|
+
msg = Message.from_dict(payload)
|
|
251
|
+
if not msg:
|
|
252
|
+
return
|
|
253
|
+
handlers_map = {
|
|
254
|
+
MessageStatus.EDITED: self._on_message_edit_handlers,
|
|
255
|
+
MessageStatus.REMOVED: self._on_message_delete_handlers,
|
|
256
|
+
}
|
|
257
|
+
if msg.status and msg.status in handlers_map:
|
|
258
|
+
for handler, filter in handlers_map[msg.status]:
|
|
259
|
+
await self._process_message_handler(handler, filter, msg)
|
|
260
|
+
for handler, filter in self._on_message_handlers:
|
|
261
|
+
await self._process_message_handler(handler, filter, msg)
|
|
262
|
+
|
|
263
|
+
async def _handle_reactions(self, data: dict):
|
|
264
|
+
if data.get("opcode") != Opcode.NOTIF_MSG_REACTIONS_CHANGED:
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
payload = data.get("payload", {})
|
|
268
|
+
chat_id = payload.get("chatId")
|
|
269
|
+
message_id = payload.get("messageId")
|
|
270
|
+
|
|
271
|
+
if not (chat_id and message_id):
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
total_count = payload.get("totalCount")
|
|
275
|
+
your_reaction = payload.get("yourReaction")
|
|
276
|
+
counters = [ReactionCounter.from_dict(c) for c in payload.get("counters", [])]
|
|
277
|
+
|
|
278
|
+
reaction_info = ReactionInfo(
|
|
279
|
+
total_count=total_count,
|
|
280
|
+
your_reaction=your_reaction,
|
|
281
|
+
counters=counters,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
for handler in self._on_reaction_change_handlers:
|
|
285
|
+
try:
|
|
286
|
+
result = handler(message_id, chat_id, reaction_info)
|
|
287
|
+
if asyncio.iscoroutine(result):
|
|
288
|
+
await result
|
|
289
|
+
except Exception as e:
|
|
290
|
+
self.logger.exception("Error in on_reaction_change_handler: %s", e)
|
|
291
|
+
|
|
292
|
+
async def _handle_chat_updates(self, data: dict) -> None:
|
|
293
|
+
if data.get("opcode") != Opcode.NOTIF_CHAT:
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
payload = data.get("payload", {})
|
|
297
|
+
chat_data = payload.get("chat", {})
|
|
298
|
+
chat = Chat.from_dict(chat_data)
|
|
299
|
+
if not chat:
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
for handler in self._on_chat_update_handlers:
|
|
303
|
+
try:
|
|
304
|
+
result = handler(chat)
|
|
305
|
+
if asyncio.iscoroutine(result):
|
|
306
|
+
await result
|
|
307
|
+
except Exception as e:
|
|
308
|
+
self.logger.exception("Error in on_chat_update_handler: %s", e)
|
|
309
|
+
|
|
310
|
+
async def _handle_raw_receive(self, data: dict[str, Any]) -> None:
|
|
311
|
+
for handler in self._on_raw_receive_handlers:
|
|
312
|
+
try:
|
|
313
|
+
result = handler(data)
|
|
314
|
+
if asyncio.iscoroutine(result):
|
|
315
|
+
await result
|
|
316
|
+
except Exception as e:
|
|
317
|
+
self.logger.exception("Error in on_raw_receive_handler: %s", e)
|
|
318
|
+
|
|
319
|
+
async def _dispatch_incoming(self, data: dict[str, Any]) -> None:
|
|
320
|
+
await self._handle_raw_receive(data)
|
|
321
|
+
await self._handle_file_upload(data)
|
|
322
|
+
await self._handle_message_notifications(data)
|
|
323
|
+
await self._handle_reactions(data)
|
|
324
|
+
await self._handle_chat_updates(data)
|
|
325
|
+
|
|
142
326
|
async def _recv_loop(self) -> None:
|
|
143
327
|
if self._socket is None:
|
|
144
328
|
self.logger.warning("Recv loop started without socket instance")
|
|
@@ -147,237 +331,35 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
147
331
|
sock = self._socket
|
|
148
332
|
loop = asyncio.get_running_loop()
|
|
149
333
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
while len(buf) < n:
|
|
154
|
-
chunk = sock.recv(n - len(buf))
|
|
155
|
-
if not chunk:
|
|
156
|
-
return bytes(buf)
|
|
157
|
-
buf.extend(chunk)
|
|
158
|
-
return bytes(buf)
|
|
334
|
+
while True:
|
|
335
|
+
try:
|
|
336
|
+
header = await self._parse_header(loop, sock)
|
|
159
337
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
try:
|
|
163
|
-
header = await loop.run_in_executor(None, lambda: _recv_exactly(10))
|
|
164
|
-
if not header or len(header) < 10:
|
|
165
|
-
self.logger.info("Socket connection closed; exiting recv loop")
|
|
166
|
-
self.is_connected = False
|
|
167
|
-
try:
|
|
168
|
-
sock.close()
|
|
169
|
-
except Exception:
|
|
170
|
-
pass # nosec B110
|
|
171
|
-
finally:
|
|
172
|
-
break
|
|
173
|
-
|
|
174
|
-
packed_len = int.from_bytes(header[6:10], "big", signed=False)
|
|
175
|
-
payload_length = packed_len & 0xFFFFFF
|
|
176
|
-
remaining = payload_length
|
|
177
|
-
payload = bytearray()
|
|
178
|
-
|
|
179
|
-
while remaining > 0:
|
|
180
|
-
min_read = min(remaining, 8192)
|
|
181
|
-
chunk = await loop.run_in_executor(
|
|
182
|
-
None, _recv_exactly, min_read
|
|
183
|
-
)
|
|
184
|
-
if not chunk:
|
|
185
|
-
self.logger.error("Connection closed while reading payload")
|
|
186
|
-
break
|
|
187
|
-
payload.extend(chunk)
|
|
188
|
-
remaining -= len(chunk)
|
|
338
|
+
if not header:
|
|
339
|
+
break
|
|
189
340
|
|
|
190
|
-
|
|
191
|
-
self.logger.error(
|
|
192
|
-
"Incomplete payload received; skipping packet"
|
|
193
|
-
)
|
|
194
|
-
continue
|
|
341
|
+
datas = await self._recv_data(loop, header, sock)
|
|
195
342
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
len(raw),
|
|
202
|
-
)
|
|
203
|
-
await asyncio.sleep(RECV_LOOP_BACKOFF_DELAY)
|
|
204
|
-
continue
|
|
343
|
+
if not datas:
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
for data_item in datas:
|
|
347
|
+
seq = data_item.get("seq")
|
|
205
348
|
|
|
206
|
-
|
|
207
|
-
if not data:
|
|
208
|
-
self.logger.warning("Failed to unpack packet, skipping")
|
|
349
|
+
if self._handle_pending(seq, data_item):
|
|
209
350
|
continue
|
|
210
351
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
[{**data, "payload": obj} for obj in payload_objs]
|
|
214
|
-
if isinstance(payload_objs, list)
|
|
215
|
-
else [data]
|
|
216
|
-
)
|
|
352
|
+
if self._incoming is not None:
|
|
353
|
+
await self._handle_incoming_queue(data_item)
|
|
217
354
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
continue
|
|
227
|
-
|
|
228
|
-
if self._incoming is not None:
|
|
229
|
-
try:
|
|
230
|
-
self._incoming.put_nowait(data_item)
|
|
231
|
-
except asyncio.QueueFull:
|
|
232
|
-
self.logger.warning(
|
|
233
|
-
"Incoming queue full; dropping message seq=%s",
|
|
234
|
-
seq,
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
data_item.get("opcode") == Opcode.NOTIF_MESSAGE
|
|
239
|
-
and self._on_message_handlers
|
|
240
|
-
):
|
|
241
|
-
try:
|
|
242
|
-
for (
|
|
243
|
-
handler,
|
|
244
|
-
filter,
|
|
245
|
-
) in self._on_message_handlers:
|
|
246
|
-
payload = data_item.get("payload", {})
|
|
247
|
-
msg_dict = (
|
|
248
|
-
payload if isinstance(payload, dict) else None
|
|
249
|
-
)
|
|
250
|
-
msg = (
|
|
251
|
-
Message.from_dict(msg_dict)
|
|
252
|
-
if msg_dict
|
|
253
|
-
else None
|
|
254
|
-
)
|
|
255
|
-
if msg and msg.status:
|
|
256
|
-
if msg.status == MessageStatus.EDITED:
|
|
257
|
-
for (
|
|
258
|
-
edit_handler,
|
|
259
|
-
edit_filter,
|
|
260
|
-
) in self._on_message_edit_handlers:
|
|
261
|
-
await self._process_message_handler(
|
|
262
|
-
handler=edit_handler,
|
|
263
|
-
filter=edit_filter,
|
|
264
|
-
message=msg,
|
|
265
|
-
)
|
|
266
|
-
elif msg.status == MessageStatus.REMOVED:
|
|
267
|
-
for (
|
|
268
|
-
remove_handler,
|
|
269
|
-
remove_filter,
|
|
270
|
-
) in self._on_message_delete_handlers:
|
|
271
|
-
await self._process_message_handler(
|
|
272
|
-
handler=remove_handler,
|
|
273
|
-
filter=remove_filter,
|
|
274
|
-
message=msg,
|
|
275
|
-
)
|
|
276
|
-
await self._process_message_handler(
|
|
277
|
-
handler=handler,
|
|
278
|
-
filter=filter,
|
|
279
|
-
message=msg,
|
|
280
|
-
)
|
|
281
|
-
except Exception:
|
|
282
|
-
self.logger.exception("Error in on_message_handler")
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
data_item.get("opcode")
|
|
286
|
-
== Opcode.NOTIF_MSG_REACTIONS_CHANGED
|
|
287
|
-
):
|
|
288
|
-
try:
|
|
289
|
-
for (
|
|
290
|
-
reaction_handler,
|
|
291
|
-
) in self._on_reaction_change_handlers:
|
|
292
|
-
payload = data_item.get("payload", {})
|
|
293
|
-
|
|
294
|
-
chat_id = payload.get("chatId")
|
|
295
|
-
message_id = payload.get("messageId")
|
|
296
|
-
|
|
297
|
-
total_count = payload.get("totalCount")
|
|
298
|
-
your_reaction = payload.get("yourReaction")
|
|
299
|
-
counters = [
|
|
300
|
-
ReactionCounter.from_dict(c)
|
|
301
|
-
for c in payload.get("counters", [])
|
|
302
|
-
]
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
chat_id
|
|
306
|
-
and message_id
|
|
307
|
-
and (
|
|
308
|
-
total_count is not None
|
|
309
|
-
or your_reaction
|
|
310
|
-
or counters
|
|
311
|
-
)
|
|
312
|
-
):
|
|
313
|
-
reaction_info = ReactionInfo(
|
|
314
|
-
total_count=total_count,
|
|
315
|
-
your_reaction=your_reaction,
|
|
316
|
-
counters=counters,
|
|
317
|
-
)
|
|
318
|
-
result = reaction_handler(
|
|
319
|
-
message_id, chat_id, reaction_info
|
|
320
|
-
)
|
|
321
|
-
if asyncio.iscoroutine(result):
|
|
322
|
-
await result
|
|
323
|
-
|
|
324
|
-
except Exception as e:
|
|
325
|
-
self.logger.exception(
|
|
326
|
-
"Error in on_reaction_change_handler: %s", e
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
if data_item.get("opcode") == Opcode.NOTIF_CHAT:
|
|
330
|
-
try:
|
|
331
|
-
for (
|
|
332
|
-
chat_update_handler,
|
|
333
|
-
) in self._on_chat_update_handlers:
|
|
334
|
-
payload = data_item.get("payload", {})
|
|
335
|
-
chat = Chat.from_dict(payload.get("chat", {}))
|
|
336
|
-
if chat:
|
|
337
|
-
result = chat_update_handler(chat)
|
|
338
|
-
if asyncio.iscoroutine(result):
|
|
339
|
-
await result
|
|
340
|
-
except Exception as e:
|
|
341
|
-
self.logger.exception(
|
|
342
|
-
"Error in on_chat_update_handler: %s", e
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
try: # TODO: переделать, временное решение
|
|
346
|
-
if data_item.get("opcode") == Opcode.NOTIF_ATTACH:
|
|
347
|
-
file_id = data_item.get("payload", {}).get(
|
|
348
|
-
"fileId", None
|
|
349
|
-
)
|
|
350
|
-
video_id = data_item.get("payload", {}).get(
|
|
351
|
-
"videoId", None
|
|
352
|
-
)
|
|
353
|
-
if file_id is not None:
|
|
354
|
-
fut = self._file_upload_waiters.pop(file_id, None)
|
|
355
|
-
if fut and not fut.done():
|
|
356
|
-
fut.set_result(data)
|
|
357
|
-
self.logger.debug(
|
|
358
|
-
"Fulfilled file upload waiter for fileId=%s",
|
|
359
|
-
file_id,
|
|
360
|
-
)
|
|
361
|
-
elif video_id is not None:
|
|
362
|
-
fut = self._file_upload_waiters.pop(video_id, None)
|
|
363
|
-
if fut and not fut.done():
|
|
364
|
-
fut.set_result(data)
|
|
365
|
-
self.logger.debug(
|
|
366
|
-
"Fulfilled file upload waiter for videoId=%s",
|
|
367
|
-
video_id,
|
|
368
|
-
)
|
|
369
|
-
except Exception:
|
|
370
|
-
self.logger.exception(
|
|
371
|
-
"Error handling file upload notification"
|
|
372
|
-
)
|
|
373
|
-
except asyncio.CancelledError:
|
|
374
|
-
self.logger.debug("Recv loop cancelled")
|
|
375
|
-
break
|
|
376
|
-
except Exception:
|
|
377
|
-
self.logger.exception("Error in recv_loop; backing off briefly")
|
|
378
|
-
await asyncio.sleep(RECV_LOOP_BACKOFF_DELAY)
|
|
379
|
-
finally:
|
|
380
|
-
self.logger.warning("<<< Recv loop exited (socket)")
|
|
355
|
+
await self._dispatch_incoming(data_item)
|
|
356
|
+
|
|
357
|
+
except asyncio.CancelledError:
|
|
358
|
+
self.logger.debug("Recv loop cancelled")
|
|
359
|
+
raise
|
|
360
|
+
except Exception:
|
|
361
|
+
self.logger.exception("Error in recv_loop; backing off briefly")
|
|
362
|
+
await asyncio.sleep(RECV_LOOP_BACKOFF_DELAY)
|
|
381
363
|
|
|
382
364
|
def _log_task_exception(self, fut: asyncio.Future[Any]) -> None:
|
|
383
365
|
try:
|
|
@@ -391,10 +373,10 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
391
373
|
async def _process_message_handler(
|
|
392
374
|
self,
|
|
393
375
|
handler: Callable[[Message], Any],
|
|
394
|
-
filter:
|
|
376
|
+
filter: BaseFilter[Message] | None,
|
|
395
377
|
message: Message,
|
|
396
378
|
) -> None:
|
|
397
|
-
if filter and not filter
|
|
379
|
+
if filter is not None and not filter(message):
|
|
398
380
|
return
|
|
399
381
|
|
|
400
382
|
result = handler(message)
|
|
@@ -427,9 +409,7 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
427
409
|
opcode=opcode.value,
|
|
428
410
|
payload=payload,
|
|
429
411
|
).model_dump(by_alias=True)
|
|
430
|
-
self.logger.debug(
|
|
431
|
-
"make_message opcode=%s cmd=%s seq=%s", opcode, cmd, self._seq
|
|
432
|
-
)
|
|
412
|
+
self.logger.debug("make_message opcode=%s cmd=%s seq=%s", opcode, cmd, self._seq)
|
|
433
413
|
return msg
|
|
434
414
|
|
|
435
415
|
@override
|
|
@@ -481,9 +461,7 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
481
461
|
raise exc from conn_err
|
|
482
462
|
raise SocketNotConnectedError from conn_err
|
|
483
463
|
except Exception as exc:
|
|
484
|
-
self.logger.exception(
|
|
485
|
-
"Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"]
|
|
486
|
-
)
|
|
464
|
+
self.logger.exception("Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"])
|
|
487
465
|
raise SocketSendError from exc
|
|
488
466
|
|
|
489
467
|
finally:
|
|
@@ -593,51 +571,46 @@ Socket connections may be unstable, SSL issues are possible.
|
|
|
593
571
|
self.logger.debug("Message queued for sending (socket)")
|
|
594
572
|
|
|
595
573
|
async def _sync(self) -> None:
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
localized_message=localized_message,
|
|
618
|
-
)
|
|
619
|
-
for raw_chat in raw_payload.get("chats", []):
|
|
620
|
-
try:
|
|
621
|
-
if raw_chat.get("type") == "DIALOG":
|
|
622
|
-
self.dialogs.append(Dialog.from_dict(raw_chat))
|
|
623
|
-
elif raw_chat.get("type") == "CHAT":
|
|
624
|
-
self.chats.append(Chat.from_dict(raw_chat))
|
|
625
|
-
elif raw_chat.get("type") == "CHANNEL":
|
|
626
|
-
self.channels.append(Channel.from_dict(raw_chat))
|
|
627
|
-
except Exception:
|
|
628
|
-
self.logger.exception("Error parsing chat entry (socket)")
|
|
629
|
-
if raw_payload.get("profile", {}).get("contact"):
|
|
630
|
-
self.me = Me.from_dict(
|
|
631
|
-
raw_payload.get("profile", {}).get("contact", {})
|
|
632
|
-
)
|
|
633
|
-
self.logger.info(
|
|
634
|
-
"Sync completed: dialogs=%d chats=%d channels=%d",
|
|
635
|
-
len(self.dialogs),
|
|
636
|
-
len(self.chats),
|
|
637
|
-
len(self.channels),
|
|
574
|
+
self.logger.info("Starting initial sync (socket)")
|
|
575
|
+
payload = SyncPayload(
|
|
576
|
+
interactive=True,
|
|
577
|
+
token=self._token,
|
|
578
|
+
chats_sync=0,
|
|
579
|
+
contacts_sync=0,
|
|
580
|
+
presence_sync=0,
|
|
581
|
+
drafts_sync=0,
|
|
582
|
+
chats_count=40,
|
|
583
|
+
).model_dump(by_alias=True)
|
|
584
|
+
data = await self._send_and_wait(opcode=Opcode.LOGIN, payload=payload)
|
|
585
|
+
raw_payload = data.get("payload", {})
|
|
586
|
+
if error := raw_payload.get("error"):
|
|
587
|
+
localized_message = raw_payload.get("localizedMessage")
|
|
588
|
+
title = raw_payload.get("title")
|
|
589
|
+
message = raw_payload.get("message")
|
|
590
|
+
raise Error(
|
|
591
|
+
error=error,
|
|
592
|
+
message=message,
|
|
593
|
+
title=title,
|
|
594
|
+
localized_message=localized_message,
|
|
638
595
|
)
|
|
639
|
-
|
|
640
|
-
|
|
596
|
+
for raw_chat in raw_payload.get("chats", []):
|
|
597
|
+
try:
|
|
598
|
+
if raw_chat.get("type") == "DIALOG":
|
|
599
|
+
self.dialogs.append(Dialog.from_dict(raw_chat))
|
|
600
|
+
elif raw_chat.get("type") == "CHAT":
|
|
601
|
+
self.chats.append(Chat.from_dict(raw_chat))
|
|
602
|
+
elif raw_chat.get("type") == "CHANNEL":
|
|
603
|
+
self.channels.append(Channel.from_dict(raw_chat))
|
|
604
|
+
except Exception:
|
|
605
|
+
self.logger.exception("Error parsing chat entry (socket)")
|
|
606
|
+
if raw_payload.get("profile", {}).get("contact"):
|
|
607
|
+
self.me = Me.from_dict(raw_payload.get("profile", {}).get("contact", {}))
|
|
608
|
+
self.logger.info(
|
|
609
|
+
"Sync completed: dialogs=%d chats=%d channels=%d",
|
|
610
|
+
len(self.dialogs),
|
|
611
|
+
len(self.chats),
|
|
612
|
+
len(self.channels),
|
|
613
|
+
)
|
|
641
614
|
|
|
642
615
|
@override
|
|
643
616
|
async def _get_chat(self, chat_id: int) -> Chat | None:
|