maxbridge-client 0.2.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.
maxbridge/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .client import MaxClient
2
+ from . import functions
3
+ from . import models
4
+ from . import exceptions
5
+
6
+ __all__ = ['MaxClient', 'functions', 'models', 'exceptions']
maxbridge/client.py ADDED
@@ -0,0 +1,504 @@
1
+ import asyncio
2
+ import itertools
3
+ import json
4
+ import logging
5
+ import uuid
6
+ from typing import Any ,Callable ,Optional
7
+
8
+ import aiohttp
9
+ import websockets
10
+ from websockets .asyncio .client import ClientConnection
11
+
12
+ from functools import wraps
13
+
14
+ from .exceptions import APIError ,ConnectionError
15
+ from . import models
16
+
17
+ WS_HOST ="wss://ws-api.oneme.ru/websocket"
18
+ RPC_VERSION =11
19
+ # Match current MAX web client fingerprint from captured traffic
20
+ APP_VERSION ="26.3.6"
21
+ USER_AGENT ="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
22
+
23
+ _logger =logging .getLogger (__name__ )
24
+
25
+
26
+ def ensure_connected (method :Callable ):
27
+ @wraps (method )
28
+ def wrapper (self ,*args ,**kwargs ):
29
+ if self ._connection is None :
30
+ raise RuntimeError ("WebSocket not connected. Call .connect() first.")
31
+ return method (self ,*args ,**kwargs )
32
+
33
+ return wrapper
34
+
35
+
36
+ class MaxClient :
37
+ def __init__ (self ):
38
+ self ._connection :Optional [ClientConnection ]=None
39
+ self ._http_pool :Optional [aiohttp .ClientSession ]=None
40
+ self ._is_logged_in :bool =False
41
+ self ._device_id :Optional [str ]=None
42
+ self ._seq =itertools .count (1 )
43
+ self ._keepalive_task :Optional [asyncio .Task ]=None
44
+ self ._recv_task :Optional [asyncio .Task ]=None
45
+ self ._incoming_event_callback =None
46
+ self ._reconnect_callback =None
47
+ self ._closed :bool =False
48
+ self ._pending ={}
49
+ self ._video_pending ={}
50
+ self ._file_pending ={}
51
+
52
+ self ._cached_chats :dict [int ,dict ]={}
53
+ self ._cached_users :dict [int ,dict ]={}
54
+ self ._profile :Optional [dict ]=None
55
+
56
+
57
+
58
+ async def connect (self ):
59
+ if self ._connection :
60
+ raise Exception ("Already connected")
61
+
62
+ self ._closed =False
63
+ _logger .info (f'Connecting to {WS_HOST }...')
64
+ self ._connection =await websockets .connect (
65
+ WS_HOST ,
66
+ origin =websockets .Origin ('https://web.max.ru'),
67
+ user_agent_header =USER_AGENT
68
+ )
69
+
70
+ self ._recv_task =asyncio .create_task (self ._recv_loop ())
71
+ _logger .info ('Connected. Receive task started.')
72
+ return self ._connection
73
+
74
+ @ensure_connected
75
+ async def disconnect (self ):
76
+ self ._closed =True
77
+ await self ._stop_keepalive_task ()
78
+ if self ._recv_task :
79
+ self ._recv_task .cancel ()
80
+ await self ._connection .close ()
81
+ self ._connection =None
82
+ if self ._http_pool :
83
+ await self ._http_pool .close ()
84
+ self ._http_pool =None
85
+
86
+ @ensure_connected
87
+ async def invoke_method (self ,opcode :int ,payload :dict [str ,Any ],retries :int =2 ):
88
+ seq =next (self ._seq )
89
+
90
+ request ={
91
+ "ver":RPC_VERSION ,
92
+ "cmd":0 ,
93
+ "seq":seq ,
94
+ "opcode":opcode ,
95
+ "payload":payload
96
+ }
97
+ _logger .info (f'-> REQUEST: {request }')
98
+
99
+ future =asyncio .get_running_loop ().create_future ()
100
+ self ._pending [seq ]=future
101
+
102
+ try :
103
+ await self ._connection .send (
104
+ json .dumps (request )
105
+ )
106
+ except websockets .exceptions .ConnectionClosed :
107
+ _logger .warning ('got ws disconnect in invoke_method')
108
+ if self ._reconnect_callback :
109
+ _logger .info ('reconnecting')
110
+ await self ._reconnect_callback ()
111
+ if retries >0 :
112
+ _logger .info ('retrying invoke_method after reconnect')
113
+ await self .invoke_method (opcode ,payload ,retries -1 )
114
+ return
115
+
116
+ try :
117
+ response =await future
118
+ except asyncio .CancelledError :
119
+ self ._pending .pop (seq ,None )
120
+ raise
121
+ _logger .info (f'<- RESPONSE: {response }')
122
+
123
+
124
+ if "error" in response .get ("payload", {}):
125
+ payload = response.get("payload", {})
126
+ error = payload.get("error")
127
+ # Some responses return a string error instead of an object
128
+ if isinstance(error, dict):
129
+ code = error.get("code", -1)
130
+ message = error.get("message", payload.get("message", "Unknown error"))
131
+ else:
132
+ code = -1
133
+ message = payload.get("message") or str(error)
134
+
135
+ raise APIError(code, message)
136
+
137
+ return response
138
+
139
+ async def set_callback (self ,function ):
140
+ import warnings
141
+ warnings .warn ('switch to set_packet_callback',category =DeprecationWarning )
142
+ self .set_packet_callback (function )
143
+
144
+ def set_packet_callback (self ,function ):
145
+ if not asyncio .iscoroutinefunction (function ):
146
+ raise TypeError ('callback must be async')
147
+ self ._incoming_event_callback =function
148
+
149
+ def set_reconnect_callback (self ,function ):
150
+ if not asyncio .iscoroutinefunction (function ):
151
+ raise TypeError ('callback must be async')
152
+ self ._reconnect_callback =function
153
+ async def debug_invoke (self ,opcode :int ,payload :dict | None =None ,timeout :float =5.0 ):
154
+ """Invoke an opcode for debugging.
155
+
156
+ This helper wraps `invoke_method` and catches exceptions, returning a
157
+ structured report instead of raising.
158
+ """
159
+ payload = payload or {}
160
+ try :
161
+ response =await asyncio .wait_for (self .invoke_method (opcode ,payload ),timeout )
162
+ return {
163
+ "opcode":opcode ,
164
+ "payload":payload ,
165
+ "response":response ,
166
+ "error":None
167
+ }
168
+ except Exception as e :
169
+ return {
170
+ "opcode":opcode ,
171
+ "payload":payload ,
172
+ "response":None ,
173
+ "error":repr (e )
174
+ }
175
+
176
+ async def discover_opcodes (self ,start :int =1 ,end :int =120 ,payloads :dict | None =None ,delay :float =0.1 ):
177
+ """Probe a range of opcodes to see what the server returns.
178
+
179
+ **Warning**: This may trigger rate limiting or disconnects.
180
+ """
181
+ payloads = payloads or {}
182
+ results = {}
183
+ for opcode in range (start ,end +1 ):
184
+ payload = payloads .get (opcode ,{})
185
+ results [opcode ]=await self .debug_invoke (opcode ,payload )
186
+ await asyncio .sleep (delay )
187
+ return results
188
+ async def _recv_loop (self ):
189
+ while not self ._closed :
190
+ try :
191
+ packet =await self ._connection .recv ()
192
+ packet =json .loads (packet )
193
+
194
+ except asyncio .CancelledError :
195
+ _logger .info ('receiver cancelled')
196
+ return
197
+
198
+ except websockets .exceptions .ConnectionClosedError as err :
199
+ _logger .warning ('got ws disconnect in receiver')
200
+ if not self ._is_logged_in :
201
+ raise err
202
+ if self ._reconnect_callback :
203
+ _logger .info ('reconnecting')
204
+ asyncio .create_task (self ._reconnect_callback ())
205
+ return
206
+
207
+ except websockets .exceptions .ConnectionClosedOK :
208
+ _logger .info ('connection closed by server')
209
+ return
210
+
211
+ except json .JSONDecodeError :
212
+ _logger .warning ('could not decode packet')
213
+ continue
214
+
215
+ seq =packet ["seq"]
216
+ future =self ._pending .pop (seq ,None )
217
+ if future :
218
+ future .set_result (packet )
219
+ continue
220
+
221
+ if packet .get ("opcode")==136 :
222
+ payload =packet .get ("payload",{})
223
+ future =None
224
+
225
+ if "videoId"in payload :
226
+ future =self ._video_pending .pop (payload ["videoId"],None )
227
+ elif "fileId"in payload :
228
+ future =self ._file_pending .pop (payload ["fileId"],None )
229
+
230
+ if future :
231
+ future .set_result (None )
232
+
233
+ if self ._incoming_event_callback :
234
+ asyncio .create_task (self ._incoming_event_callback (self ,packet ))
235
+
236
+
237
+
238
+ @ensure_connected
239
+ async def _send_keepalive_packet (self ):
240
+ try :
241
+ async with asyncio .timeout (15 ):
242
+ await self .invoke_method (
243
+ opcode =1 ,
244
+ payload ={"interactive":True }
245
+ )
246
+ except asyncio .TimeoutError :
247
+ _logger .warning ('keepalive ping timed out')
248
+ if self ._reconnect_callback :
249
+ _logger .info ('reconnecting')
250
+ asyncio .create_task (self ._reconnect_callback ())
251
+
252
+ @ensure_connected
253
+ async def _keepalive_loop (self ):
254
+ _logger .info (f'keepalive task started')
255
+ try :
256
+ while True :
257
+ await self ._send_keepalive_packet ()
258
+ await asyncio .sleep (30 )
259
+ except asyncio .CancelledError :
260
+ _logger .info ('keepalive task stopped')
261
+ return
262
+
263
+ @ensure_connected
264
+ async def _start_keepalive_task (self ):
265
+ if self ._keepalive_task :
266
+ raise Exception ('Keepalive task already started')
267
+
268
+ self ._keepalive_task =asyncio .create_task (self ._keepalive_loop ())
269
+ return
270
+
271
+ async def _stop_keepalive_task (self ):
272
+ if not self ._keepalive_task :
273
+ raise Exception ('Keepalive task is not running')
274
+
275
+ self ._keepalive_task .cancel ()
276
+ self ._keepalive_task =None
277
+ return
278
+
279
+
280
+
281
+ @ensure_connected
282
+ async def _send_hello_packet (self ,device_id :Optional [str ]=None ):
283
+ self ._device_id =device_id or f'{uuid .uuid4 ()}'
284
+ return await self .invoke_method (
285
+ opcode =6 ,
286
+ payload ={
287
+ "userAgent":{
288
+ "deviceType":"WEB",
289
+ "locale":"ru",
290
+ "deviceLocale":"ru",
291
+ "osVersion":"Linux",
292
+ "deviceName":"Chrome",
293
+ "headerUserAgent":USER_AGENT ,
294
+ "appVersion":APP_VERSION ,
295
+ "screen":"720x1280 1.0x",
296
+ "timezone":"Asia/Yekaterinburg"
297
+ },
298
+ "deviceId":self ._device_id ,
299
+ }
300
+ )
301
+
302
+ @ensure_connected
303
+ async def send_code (self ,phone :str )->str :
304
+ """:returns: Login token."""
305
+ await self ._send_hello_packet ()
306
+ start_auth_response =await self .invoke_method (
307
+ opcode =17 ,
308
+ payload ={
309
+ "phone":phone ,
310
+ "type":"START_AUTH",
311
+ "language":"ru"
312
+ }
313
+ )
314
+ return start_auth_response ["payload"]["token"]
315
+
316
+ @ensure_connected
317
+ async def sign_in (self ,sms_token :str ,sms_code :int ):
318
+ """
319
+ Auth token for further login is at ['payload']['tokenAttrs']['LOGIN']['token']
320
+ :param login_token: Must be obtained via `send_code`.
321
+ """
322
+ verification_response =await self .invoke_method (
323
+ opcode =18 ,
324
+ payload ={
325
+ "token":sms_token ,
326
+ "verifyCode":str (sms_code ),
327
+ "authTokenType":"CHECK_CODE"
328
+ }
329
+ )
330
+
331
+ if "error"in verification_response ["payload"]:
332
+ raise Exception (verification_response ["payload"]["error"])
333
+
334
+
335
+ if "profile"in verification_response ["payload"]:
336
+ self ._profile =verification_response ["payload"]["profile"]
337
+
338
+
339
+ if "chats"in verification_response ["payload"]:
340
+ for chat in verification_response ["payload"]["chats"]:
341
+ self ._cached_chats [chat ["id"]]=chat
342
+
343
+
344
+ if "chats"in verification_response ["payload"]:
345
+ user_ids =set ()
346
+ for chat in verification_response ["payload"]["chats"]:
347
+ if "participants"in chat :
348
+ for uid in chat ["participants"].keys ():
349
+ user_ids .add (int (uid ))
350
+ if user_ids :
351
+
352
+ try :
353
+ users_response =await self .invoke_method (
354
+ opcode =32 ,
355
+ payload ={"contactIds":list (user_ids )}
356
+ )
357
+ if "payload"in users_response and "contacts"in users_response ["payload"]:
358
+ for user in users_response ["payload"]["contacts"]:
359
+ self ._cached_users [user ["id"]]=user
360
+ except Exception as e :
361
+ _logger .warning (f'Failed to fetch users: {e }')
362
+
363
+ try :
364
+ phone =verification_response ["payload"]["profile"]["contact"]["phone"]
365
+ except :
366
+ phone ='[?]'
367
+ _logger .warning ('Got no phone number in server response')
368
+ _logger .info (f'Successfully logged in as {phone }')
369
+
370
+ self ._is_logged_in =True
371
+ await self ._start_keepalive_task ()
372
+
373
+ return verification_response
374
+
375
+ @ensure_connected
376
+ async def login_by_token (self ,token :str ,device_id :Optional [str ]=None ):
377
+ await self ._send_hello_packet (device_id )
378
+ _logger .info ("using session")
379
+ login_response =await self .invoke_method (
380
+ opcode =19 ,
381
+ payload ={
382
+ "interactive":True ,
383
+ "token":token ,
384
+ "chatsCount":40 ,
385
+ "chatsSync":0 ,
386
+ "contactsSync":0 ,
387
+ "presenceSync":-1 ,
388
+ "draftsSync":0
389
+ }
390
+ )
391
+
392
+ if "error"in login_response ["payload"]:
393
+ raise Exception (login_response ["payload"]["error"])
394
+
395
+
396
+ if "profile"in login_response ["payload"]:
397
+ self ._profile =login_response ["payload"]["profile"]
398
+
399
+
400
+ if "chats"in login_response ["payload"]:
401
+ for chat in login_response ["payload"]["chats"]:
402
+ self ._cached_chats [chat ["id"]]=chat
403
+
404
+
405
+ if "chats"in login_response ["payload"]:
406
+ user_ids =set ()
407
+ for chat in login_response ["payload"]["chats"]:
408
+ if "participants"in chat :
409
+ for uid in chat ["participants"].keys ():
410
+ user_ids .add (int (uid ))
411
+ if user_ids :
412
+
413
+ try :
414
+ users_response =await self .invoke_method (
415
+ opcode =32 ,
416
+ payload ={"contactIds":list (user_ids )}
417
+ )
418
+ if "payload"in users_response and "contacts"in users_response ["payload"]:
419
+ for user in users_response ["payload"]["contacts"]:
420
+ self ._cached_users [user ["id"]]=user
421
+ except Exception as e :
422
+ _logger .warning (f'Failed to fetch users: {e }')
423
+
424
+ try :
425
+ phone =login_response ["payload"]["profile"]["contact"]["phone"]
426
+ except :
427
+ phone ='[?]'
428
+ _logger .warning ('Got no phone number in server response')
429
+ _logger .info (f'Successfully logged in as {phone }')
430
+
431
+ self ._is_logged_in =True
432
+ await self ._start_keepalive_task ()
433
+
434
+ return login_response
435
+
436
+ @property
437
+ def device_id (self )->Optional [str ]:
438
+ return self ._device_id
439
+
440
+ @property
441
+ def profile (self )->Optional [dict ]:
442
+ return self ._profile
443
+
444
+ def get_cached_chats (self )->dict [int ,dict ]:
445
+ return self ._cached_chats
446
+
447
+ def get_cached_users (self )->dict [int ,dict ]:
448
+ return self ._cached_users
449
+
450
+ def get_chats_structured (self )->dict [int ,models .Chat ]:
451
+ """Return cached chats as `Chat` models."""
452
+ return {cid :models .Chat .from_raw (data )for cid ,data in self ._cached_chats .items ()}
453
+
454
+ def get_users_structured (self )->dict [int ,models .User ]:
455
+ """Return cached users as `User` models."""
456
+ return {uid :models .User .from_raw (data )for uid ,data in self ._cached_users .items ()}
457
+
458
+ @ensure_connected
459
+ async def get_chat_messages (self ,chat_id :int ,from_ts :Optional [int ]=None ,backward :int =30 ,forward :int =0 ,get_messages :bool =True ):
460
+ """Get messages from chat using the same opcode/shape as web client.
461
+
462
+ :param chat_id: Chat identifier.
463
+ :param from_ts: Anchor timestamp (ms). If not provided, tries to use lastEventTime/lastMessage.time from cache.
464
+ :param backward: How many messages to request before ``from_ts``.
465
+ :param forward: How many messages to request after ``from_ts``.
466
+ :param get_messages: Whether to include messages in the response (web uses ``true``).
467
+ """
468
+ if from_ts is None :
469
+ cached =self ._cached_chats .get (chat_id )
470
+ if cached :
471
+ from_ts =cached .get ("lastEventTime")or cached .get ("lastMessage" ,{}).get ("time")
472
+ if from_ts is None :
473
+ # Fallback to zero – server will interpret according to its defaults
474
+ from_ts =0
475
+
476
+ return await self .invoke_method (
477
+ opcode =49 ,
478
+ payload ={
479
+ "chatId":chat_id ,
480
+ "from":from_ts ,
481
+ "forward":forward ,
482
+ "backward":backward ,
483
+ "getMessages":get_messages
484
+ }
485
+ )
486
+
487
+ @ensure_connected
488
+ async def get_chat_messages_structured (self ,chat_id :int ,from_ts :Optional [int ]=None ,backward :int =30 ,forward :int =0 )->list [models .Message ]:
489
+ """Wrapper over `get_chat_messages` that returns a list of `Message` models."""
490
+ raw =await self .get_chat_messages (chat_id ,from_ts =from_ts ,backward =backward ,forward =forward ,get_messages =True )
491
+ payload =raw .get ("payload", {})
492
+ msgs =payload .get ("messages")or []
493
+ if isinstance (msgs ,dict ):
494
+ iterable =msgs .values ()
495
+ else :
496
+ iterable =msgs
497
+ return [models .Message .from_raw (m ,chat_id =chat_id )for m in iterable ]
498
+
499
+ async def __aenter__ (self ):
500
+ await self .connect ()
501
+ return self
502
+
503
+ async def __aexit__ (self ,exc_type ,exc_val ,exc_tb ):
504
+ await self .disconnect ()
@@ -0,0 +1,17 @@
1
+ class MaxException(Exception):
2
+ pass
3
+
4
+
5
+ class ConnectionError(MaxException):
6
+ pass
7
+
8
+
9
+ class AuthenticationError(MaxException):
10
+ pass
11
+
12
+
13
+ class APIError(MaxException):
14
+ def __init__(self, error_code: int, message: str):
15
+ self.error_code = error_code
16
+ self.message = message
17
+ super().__init__(f"API Error {error_code}: {message}")
maxbridge/models.py ADDED
@@ -0,0 +1,80 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional ,List ,Dict ,Any
3
+
4
+
5
+ @dataclass
6
+ class User :
7
+ id :int
8
+ name :str
9
+ username :Optional [str ]=None
10
+ avatar :Optional [str ]=None
11
+
12
+ @classmethod
13
+ def from_raw (cls ,raw :Dict [str ,Any ]):
14
+ contact_id =raw .get ("id")
15
+ names =raw .get ("names")or []
16
+ name =names [0 ].get ("name") if names else str (contact_id )
17
+ username =raw .get ("username")
18
+ base_url =raw .get ("baseUrl") or raw .get ("baseRawUrl")
19
+ avatar =None
20
+ if base_url :
21
+ avatar =base_url
22
+ return cls (
23
+ id =contact_id ,
24
+ name =name ,
25
+ username =username ,
26
+ avatar =avatar ,
27
+ )
28
+
29
+
30
+ @dataclass
31
+ class Chat :
32
+ id :int
33
+ title :str
34
+ type :str
35
+ participants_count :Optional [int ]=None
36
+ avatar :Optional [str ]=None
37
+
38
+ @classmethod
39
+ def from_raw (cls ,raw :Dict [str ,Any ]):
40
+ chat_id =raw .get ("id")
41
+ title =raw .get ("title")
42
+ chat_type =raw .get ("type","UNKNOWN")
43
+ participants =raw .get ("participants")or {}
44
+ participants_count =len (participants )
45
+ avatar =raw .get ("avatar") or raw .get ("baseUrl") or raw .get ("baseRawUrl")
46
+ if not title and chat_type =='DIALOG':
47
+ # диалоги обычно без своего title, его удобно подставлять снаружи по собеседнику
48
+ title =f"Dialog {chat_id }"
49
+ return cls (
50
+ id =chat_id ,
51
+ title =title or "" ,
52
+ type =chat_type ,
53
+ participants_count =participants_count or None ,
54
+ avatar =avatar ,
55
+ )
56
+
57
+
58
+ @dataclass
59
+ class Message :
60
+ id :str
61
+ chat_id :int
62
+ user_id :int
63
+ text :str
64
+ timestamp :int
65
+ attaches :List [Dict [str ,Any ]]=None
66
+
67
+ def __post_init__ (self ):
68
+ if self .attaches is None :
69
+ self .attaches =[]
70
+
71
+ @classmethod
72
+ def from_raw (cls ,raw :Dict [str ,Any ] ,chat_id :Optional [int ]=None ):
73
+ return cls (
74
+ id =raw .get ("id" ),
75
+ chat_id =chat_id if chat_id is not None else int (raw .get ("chatId" ,0 )),
76
+ user_id =raw .get ("sender" ),
77
+ text =raw .get ("text" ,""),
78
+ timestamp =raw .get ("time" ,0 ),
79
+ attaches =raw .get ("attaches" )or [],
80
+ )
maxbridge/packet.py ADDED
@@ -0,0 +1,12 @@
1
+ from dataclasses import dataclass
2
+ from typing import Literal ,Any
3
+
4
+
5
+ @dataclass
6
+ class MaxPacket :
7
+
8
+ ver :int
9
+ cmd :Literal [0 ,1 ]
10
+ opcode :int
11
+ seq :int
12
+ payload :dict [str ,Any ]
@@ -0,0 +1,299 @@
1
+ Metadata-Version: 2.4
2
+ Name: maxbridge-client
3
+ Version: 0.2.0
4
+ Summary: Асинхронная Python библиотека для MAX API
5
+ Home-page: https://github.com/username/max-bridge
6
+ Author: VK Max Client Team
7
+ Author-email:
8
+ Maintainer: VK Max Client Team
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 MaxBridge contributors
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ Project-URL: Homepage, https://github.com/username/max-bridge
31
+ Project-URL: Documentation, https://github.com/username/max-bridge/docs/
32
+ Project-URL: Repository, https://github.com/username/max-bridge
33
+ Project-URL: Bug Reports, https://github.com/username/max-bridge/issues
34
+ Project-URL: Source, https://github.com/username/max-bridge
35
+ Keywords: vk,max,messenger,api,websocket,async
36
+ Classifier: Development Status :: 3 - Alpha
37
+ Classifier: Intended Audience :: Developers
38
+ Classifier: License :: OSI Approved :: MIT License
39
+ Classifier: Operating System :: OS Independent
40
+ Classifier: Programming Language :: Python :: 3
41
+ Classifier: Programming Language :: Python :: 3.9
42
+ Classifier: Programming Language :: Python :: 3.10
43
+ Classifier: Programming Language :: Python :: 3.11
44
+ Classifier: Programming Language :: Python :: 3.12
45
+ Classifier: Topic :: Communications :: Chat
46
+ Classifier: Topic :: Internet :: WWW/HTTP
47
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
48
+ Requires-Python: >=3.9
49
+ Description-Content-Type: text/markdown
50
+ License-File: LICENSE
51
+ Requires-Dist: aiohttp>=3.8.0
52
+ Requires-Dist: websockets>=12.0
53
+ Provides-Extra: dev
54
+ Requires-Dist: black; extra == "dev"
55
+ Requires-Dist: pytest; extra == "dev"
56
+ Requires-Dist: pytest-asyncio; extra == "dev"
57
+ Requires-Dist: mypy; extra == "dev"
58
+ Dynamic: home-page
59
+ Dynamic: license-file
60
+ Dynamic: requires-python
61
+
62
+ # MaxBridge
63
+
64
+ [![Python](https://img.shields.io/badge/Python-3.8+-blue.svg)](https://www.python.org/)
65
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
66
+ [![Asyncio](https://img.shields.io/badge/Asyncio-Supported-green.svg)](https://docs.python.org/3/library/asyncio.html)
67
+
68
+ Асинхронная Python библиотека для взаимодействия с API мессенджера MAX через WebSocket соединение.
69
+
70
+ ## Описание
71
+
72
+ MaxBridge предоставляет удобный интерфейс для работы с мессенджером MAX, позволяя отправлять и получать сообщения, управлять чатами, пользователями и файлами в асинхронном режиме.
73
+
74
+ ## 🚀 Быстрый старт
75
+
76
+ ```python
77
+ import asyncio
78
+ from maxbridge import MaxClient
79
+
80
+ async def main():
81
+ async with MaxClient() as client:
82
+ # Авторизация по токену
83
+ await client.login_by_token("ваш_токен")
84
+
85
+ # Получение списка чатов
86
+ chats = client.get_cached_chats()
87
+ print(f"Найдено чатов: {len(chats)}")
88
+
89
+ asyncio.run(main())
90
+ ```
91
+
92
+ ## 📦 Установка
93
+
94
+ ```bash
95
+ # Установка из PyPI (предполагается, что пакет опубликован)
96
+ pip install maxbridge
97
+
98
+ # Или из исходников
99
+ git clone <repository-url>
100
+ cd max-bridge
101
+ pip install -e .
102
+ ```
103
+
104
+ ## 🔐 Авторизация
105
+
106
+ ### По токену
107
+
108
+ **Важно:** Для авторизации необходимо получить токен доступа из веб-версии MAX.
109
+
110
+ #### Как получить токен:
111
+
112
+ 1. Откройте https://web.max.ru в браузере
113
+ 2. Авторизуйтесь в вашем аккаунте
114
+ 3. Откройте DevTools (F12) → Application → Local Storage → https://web.max.ru
115
+ 4. Найдите ключ `token` и скопируйте его значение
116
+
117
+ ```python
118
+ async with MaxClient() as client:
119
+ await client.login_by_token("ваш_длинный_токен")
120
+ ```
121
+
122
+ **Примечание:** Токен является конфиденциальной информацией. Храните его securely.
123
+
124
+ ## 💬 Работа с чатами
125
+
126
+ ### Получение списка чатов
127
+
128
+ ```python
129
+ chats = client.get_cached_chats()
130
+ for chat_id, chat_info in chats.items():
131
+ print(f"ID: {chat_id}, Тип: {chat_info['type']}, Название: {chat_info.get('title', 'Диалог')}")
132
+ ```
133
+
134
+ ### Получение сообщений чата
135
+
136
+ ```python
137
+ messages = await client.get_chat_messages(chat_id=123456, count=50, offset=0)
138
+ if "payload" in messages and "messages" in messages["payload"]:
139
+ for msg_id, msg in messages["payload"]["messages"].items():
140
+ print(f"{msg['sender']}: {msg['text']}")
141
+ ```
142
+
143
+ ## 📨 Отправка сообщений
144
+
145
+ ```python
146
+ from maxbridge.functions import messages
147
+
148
+ # Текстовое сообщение
149
+ await messages.send_message(client, chat_id=123456, text="Привет!")
150
+
151
+ # С фото
152
+ await messages.send_photo(client, chat_id=123456, image_path="photo.jpg", caption="Фото")
153
+
154
+ # Реплай
155
+ await messages.reply_message(client, chat_id=123456, text="Ответ", reply_to_message_id="msg_id")
156
+ ```
157
+
158
+ ## 👥 Управление пользователями
159
+
160
+ ```python
161
+ from maxbridge.functions import users
162
+
163
+ # Информация о пользователях
164
+ user_info = await users.resolve_users(client, user_ids=[12345, 67890])
165
+
166
+ # Добавить в контакты
167
+ await users.add_to_contacts(client, user_id=12345)
168
+ ```
169
+
170
+ ## 📁 Работа с файлами
171
+
172
+ ```python
173
+ from maxbridge.functions import messages, uploads
174
+
175
+ # Отправка файла
176
+ await messages.send_file(client, chat_id=123456, file_path="document.pdf", caption="Документ")
177
+
178
+ # Скачивание файла
179
+ download_url = await uploads.download_file(
180
+ client, chat_id=123456, message_id="msg_id", file_id=789
181
+ )
182
+ ```
183
+
184
+ ## 📺 Работа с медиа
185
+
186
+ ```python
187
+ # Скачивание видео
188
+ video_url = await uploads.download_video(
189
+ client, chat_id=123456, message_id="msg_id", video_id=101112
190
+ )
191
+ ```
192
+
193
+ ## 👥 Группы и каналы
194
+
195
+ ```python
196
+ from maxbridge.functions import groups, channels
197
+
198
+ # Создание группы
199
+ await groups.create_group(client, "Название группы", participant_ids=[123, 456])
200
+
201
+ # Присоединение к каналу
202
+ await channels.join_channel(client, "username_канала")
203
+
204
+ # Информация о канале
205
+ channel_info = await channels.resolve_channel_username(client, "username")
206
+ ```
207
+
208
+ ## 🛠️ Обработка ошибок
209
+
210
+ ```python
211
+ from maxbridge.exceptions import APIError, ConnectionError
212
+
213
+ try:
214
+ await client.login_by_token("токен")
215
+ except APIError as e:
216
+ print(f"Ошибка API {e.error_code}: {e.message}")
217
+ except ConnectionError:
218
+ print("Ошибка подключения")
219
+ ```
220
+
221
+ ## 📊 Модели данных
222
+
223
+ ```python
224
+ from maxbridge.models import User, Chat, Message
225
+
226
+ # Примеры
227
+ user = User(id=123, name="Имя", username="username")
228
+ chat = Chat(id=456, title="Название", type="DIALOG")
229
+ message = Message(id="msg_id", chat_id=456, user_id=123, text="Текст")
230
+ ```
231
+
232
+ ## 🔧 Продвинутые возможности
233
+
234
+ ### Обработка событий в реальном времени
235
+
236
+ ```python
237
+ def event_handler(client, packet):
238
+ if packet.get("opcode") == 64: # Новое сообщение
239
+ print("Новое сообщение!")
240
+
241
+ client.set_packet_callback(event_handler)
242
+ ```
243
+
244
+ ### Кастомные настройки
245
+
246
+ ```python
247
+ # Профиль
248
+ profile = client.profile
249
+
250
+ # Кэшированные пользователи
251
+ users = client.get_cached_users()
252
+ ```
253
+
254
+ ## 📚 Документация
255
+
256
+ - [API Reference](docs/API.md)
257
+ - [Примеры](examples/)
258
+ - [Contributing](CONTRIBUTING.md)
259
+ - [Changelog](CHANGELOG.md)
260
+
261
+ ## 🏗️ Структура проекта
262
+
263
+ ```
264
+ maxbridge/
265
+ ├── __init__.py
266
+ ├── client.py # WebSocket клиент
267
+ ├── models.py # Модели данных
268
+ ├── exceptions.py # Исключения
269
+ ├── packet.py # Обработка пакетов
270
+ └── functions/ # API функции
271
+ ├── __init__.py
272
+ ├── messages.py # Сообщения
273
+ ├── users.py # Пользователи
274
+ ├── groups.py # Группы
275
+ ├── channels.py # Каналы
276
+ ├── profile.py # Профиль
277
+ └── uploads.py # Загрузки
278
+
279
+ docs/ # Документация
280
+ examples/ # Примеры
281
+ ```
282
+
283
+ ## ⚠️ Важные замечания
284
+
285
+ - Все методы асинхронные — используйте `await`
286
+ - Рекомендуется `async with MaxClient() as client:`
287
+ - Токены имеют срок действия
288
+ - Соблюдайте лимиты API
289
+
290
+ ## 🐛 Отладка
291
+
292
+ ```python
293
+ import logging
294
+ logging.basicConfig(level=logging.INFO)
295
+ ```
296
+
297
+ ## 📄 Лицензия
298
+
299
+ MIT License
@@ -0,0 +1,10 @@
1
+ maxbridge/__init__.py,sha256=_V805CBVvD7aq0tLHkDVj29-AJero0AmLhV8GvEwIiY,161
2
+ maxbridge/client.py,sha256=MM_UZobtThTDw6Ph0c170SlFUqf4WxzMTbKCQC4sGu4,17792
3
+ maxbridge/exceptions.py,sha256=3SxYA381VWcJE4SkIsd1UWVTeoXAgZJYlbahEZ_jfCo,357
4
+ maxbridge/models.py,sha256=OzElV11wezwdFj4zmelBYVP0NwIeAHKvrkKO0sBDNAs,2318
5
+ maxbridge/packet.py,sha256=gXiE1p1NbcoBveUk0aQjO28JNUtjtTv4eZ-q_CiNnd8,195
6
+ maxbridge_client-0.2.0.dist-info/licenses/LICENSE,sha256=Piagsiu7OcH67ZrpAJBxR_-tPhJQHi07CMWgALgmLyU,1078
7
+ maxbridge_client-0.2.0.dist-info/METADATA,sha256=g_W_uTVWOgEyJ11QuVdnzsYHsZcuvxA9awVRy1WZ_dg,10116
8
+ maxbridge_client-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ maxbridge_client-0.2.0.dist-info/top_level.txt,sha256=pcPHYIca6HlkF7wuoRfuXgzXDXty6V3-VtGMha7Jjd4,10
10
+ maxbridge_client-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MaxBridge contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ maxbridge