maxapi-python 1.2.4__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. maxapi_python-2.0.0.dist-info/METADATA +217 -0
  2. maxapi_python-2.0.0.dist-info/RECORD +140 -0
  3. {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +50 -105
  5. pymax/api/__init__.py +17 -0
  6. pymax/api/auth/__init__.py +1 -0
  7. pymax/api/auth/enums.py +17 -0
  8. pymax/api/auth/payloads.py +129 -0
  9. pymax/api/auth/service.py +313 -0
  10. pymax/api/auth/types.py +13 -0
  11. pymax/api/chats/__init__.py +8 -0
  12. pymax/api/chats/enums.py +27 -0
  13. pymax/api/chats/payloads.py +103 -0
  14. pymax/api/chats/service.py +277 -0
  15. pymax/api/facade.py +32 -0
  16. pymax/api/messages/__init__.py +1 -0
  17. pymax/api/messages/enums.py +17 -0
  18. pymax/api/messages/payloads.py +92 -0
  19. pymax/api/messages/service.py +337 -0
  20. pymax/api/models.py +13 -0
  21. pymax/api/response.py +123 -0
  22. pymax/api/self/__init__.py +2 -0
  23. pymax/api/self/enums.py +11 -0
  24. pymax/api/self/payloads.py +41 -0
  25. pymax/api/self/service.py +142 -0
  26. pymax/api/session/__init__.py +1 -0
  27. pymax/api/session/enums.py +10 -0
  28. pymax/api/session/payloads.py +76 -0
  29. pymax/api/session/service.py +72 -0
  30. pymax/api/uploads/__init__.py +1 -0
  31. pymax/api/uploads/models.py +49 -0
  32. pymax/api/uploads/payloads.py +25 -0
  33. pymax/api/uploads/service.py +458 -0
  34. pymax/api/users/__init__.py +2 -0
  35. pymax/api/users/enums.py +12 -0
  36. pymax/api/users/payloads.py +16 -0
  37. pymax/api/users/service.py +124 -0
  38. pymax/app.py +273 -0
  39. pymax/auth/__init__.py +25 -0
  40. pymax/auth/base.py +37 -0
  41. pymax/auth/email.py +0 -0
  42. pymax/auth/models.py +5 -0
  43. pymax/auth/providers.py +127 -0
  44. pymax/auth/qr.py +135 -0
  45. pymax/auth/service.py +25 -0
  46. pymax/auth/sms.py +122 -0
  47. pymax/base.py +204 -0
  48. pymax/client.py +106 -0
  49. pymax/client_web.py +83 -0
  50. pymax/config.py +215 -0
  51. pymax/connection/__init__.py +1 -0
  52. pymax/connection/connection.py +205 -0
  53. pymax/connection/pending.py +46 -0
  54. pymax/connection/readers/__init__.py +2 -0
  55. pymax/connection/readers/base.py +6 -0
  56. pymax/connection/readers/tcp.py +29 -0
  57. pymax/connection/readers/ws.py +14 -0
  58. pymax/dispatch/__init__.py +10 -0
  59. pymax/dispatch/dispatcher.py +222 -0
  60. pymax/dispatch/enums.py +12 -0
  61. pymax/dispatch/mapping.py +73 -0
  62. pymax/dispatch/resolvers.py +52 -0
  63. pymax/dispatch/router.py +216 -0
  64. pymax/exceptions.py +22 -89
  65. pymax/files/__init__.py +9 -0
  66. pymax/files/base.py +82 -0
  67. pymax/files/file.py +76 -0
  68. pymax/files/photo.py +108 -0
  69. pymax/files/static.py +10 -0
  70. pymax/files/video.py +74 -0
  71. pymax/formatting/__init__.py +0 -0
  72. pymax/formatting/markdown.py +217 -0
  73. pymax/infra/__init__.py +1 -0
  74. pymax/infra/auth.py +55 -0
  75. pymax/infra/base.py +15 -0
  76. pymax/infra/chat.py +240 -0
  77. pymax/infra/message.py +252 -0
  78. pymax/infra/protocol.py +9 -0
  79. pymax/infra/self.py +139 -0
  80. pymax/infra/user.py +107 -0
  81. pymax/logging.py +129 -0
  82. pymax/protocol/__init__.py +11 -0
  83. pymax/protocol/base.py +13 -0
  84. pymax/{static/enum.py → protocol/enums.py} +36 -79
  85. pymax/protocol/models.py +33 -0
  86. pymax/protocol/tcp/__init__.py +1 -0
  87. pymax/protocol/tcp/compression.py +97 -0
  88. pymax/protocol/tcp/framing.py +68 -0
  89. pymax/protocol/tcp/payload.py +127 -0
  90. pymax/protocol/tcp/protocol.py +68 -0
  91. pymax/protocol/ws/__init__.py +1 -0
  92. pymax/protocol/ws/protocol.py +27 -0
  93. pymax/py.typed +0 -0
  94. pymax/routers.py +8 -0
  95. pymax/session/__init__.py +3 -0
  96. pymax/session/models.py +11 -0
  97. pymax/session/protocol.py +14 -0
  98. pymax/session/store.py +232 -0
  99. pymax/telemetry/__init__.py +3 -0
  100. pymax/telemetry/navigation.py +181 -0
  101. pymax/telemetry/payloads.py +142 -0
  102. pymax/telemetry/service.py +225 -0
  103. pymax/transport/__init__.py +0 -0
  104. pymax/transport/base.py +14 -0
  105. pymax/transport/tcp.py +93 -0
  106. pymax/transport/websocket.py +50 -0
  107. pymax/types/__init__.py +2 -0
  108. pymax/types/domain/__init__.py +11 -0
  109. pymax/types/domain/attachments/__init__.py +11 -0
  110. pymax/types/domain/attachments/audio.py +35 -0
  111. pymax/types/domain/attachments/call.py +26 -0
  112. pymax/types/domain/attachments/contact.py +32 -0
  113. pymax/types/domain/attachments/control.py +20 -0
  114. pymax/types/domain/attachments/enums.py +27 -0
  115. pymax/types/domain/attachments/file.py +56 -0
  116. pymax/types/domain/attachments/keyboards/__init__.py +1 -0
  117. pymax/types/domain/attachments/keyboards/inline.py +19 -0
  118. pymax/types/domain/attachments/photo.py +45 -0
  119. pymax/types/domain/attachments/share.py +29 -0
  120. pymax/types/domain/attachments/sticker.py +50 -0
  121. pymax/types/domain/attachments/video.py +90 -0
  122. pymax/types/domain/auth.py +161 -0
  123. pymax/types/domain/base.py +17 -0
  124. pymax/types/domain/chat.py +426 -0
  125. pymax/types/domain/element.py +24 -0
  126. pymax/types/domain/enums.py +24 -0
  127. pymax/types/domain/error.py +20 -0
  128. pymax/types/domain/folder.py +74 -0
  129. pymax/types/domain/login.py +35 -0
  130. pymax/types/domain/message.py +378 -0
  131. pymax/types/domain/name.py +20 -0
  132. pymax/types/domain/profile.py +15 -0
  133. pymax/types/domain/session.py +52 -0
  134. pymax/types/domain/sync.py +80 -0
  135. pymax/types/domain/user.py +117 -0
  136. pymax/types/events/__init__.py +3 -0
  137. pymax/types/events/file.py +5 -0
  138. pymax/types/events/message.py +37 -0
  139. pymax/types/events/video.py +5 -0
  140. maxapi_python-1.2.4.dist-info/METADATA +0 -205
  141. maxapi_python-1.2.4.dist-info/RECORD +0 -33
  142. pymax/core.py +0 -390
  143. pymax/crud.py +0 -96
  144. pymax/files.py +0 -138
  145. pymax/filters.py +0 -164
  146. pymax/formatter.py +0 -31
  147. pymax/formatting.py +0 -74
  148. pymax/interfaces.py +0 -552
  149. pymax/mixins/__init__.py +0 -40
  150. pymax/mixins/auth.py +0 -368
  151. pymax/mixins/channel.py +0 -130
  152. pymax/mixins/group.py +0 -458
  153. pymax/mixins/handler.py +0 -285
  154. pymax/mixins/message.py +0 -879
  155. pymax/mixins/scheduler.py +0 -28
  156. pymax/mixins/self.py +0 -259
  157. pymax/mixins/socket.py +0 -297
  158. pymax/mixins/telemetry.py +0 -112
  159. pymax/mixins/user.py +0 -219
  160. pymax/mixins/websocket.py +0 -142
  161. pymax/models.py +0 -8
  162. pymax/navigation.py +0 -187
  163. pymax/payloads.py +0 -367
  164. pymax/protocols.py +0 -123
  165. pymax/static/constant.py +0 -89
  166. pymax/types.py +0 -1220
  167. pymax/utils.py +0 -90
  168. {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from random import Random
5
+ from typing import Any
6
+
7
+ from pydantic import Field
8
+
9
+ from pymax.api.models import CamelModel
10
+
11
+
12
+ class TelemetryEvent(CamelModel):
13
+ time: int
14
+ user_id: int
15
+ type: str
16
+ event: str
17
+ params: dict[str, Any] = Field(default_factory=dict)
18
+ session_id: int
19
+
20
+
21
+ class TelemetryPayload(CamelModel):
22
+ events: list[TelemetryEvent]
23
+
24
+
25
+ class TelemetryPayloadBuilder:
26
+ def __init__(self, rng: Random) -> None:
27
+ self.rng = rng
28
+
29
+ def login(self, user_id: int, session_id: int) -> TelemetryEvent:
30
+ return TelemetryEvent(
31
+ time=now_ms(),
32
+ user_id=user_id,
33
+ type="PERF",
34
+ event="login",
35
+ params={
36
+ "properties": {
37
+ "connection_type": 2,
38
+ "vpn": 0,
39
+ "class": 2,
40
+ "background": 1,
41
+ "warm_start": 1,
42
+ },
43
+ "errorType": 100,
44
+ },
45
+ session_id=session_id,
46
+ )
47
+
48
+ def navigation(
49
+ self,
50
+ *,
51
+ user_id: int,
52
+ session_id: int,
53
+ screen_from: int,
54
+ screen_to: int,
55
+ prev_time: int,
56
+ action_id: int,
57
+ extra_params: dict[str, int],
58
+ ) -> TelemetryEvent:
59
+ params: dict[str, Any] = {
60
+ "prev_time": prev_time,
61
+ "screen_to": screen_to,
62
+ "action_id": action_id,
63
+ "screen_from": screen_from,
64
+ }
65
+ params.update(extra_params)
66
+ return TelemetryEvent(
67
+ time=now_ms(),
68
+ user_id=user_id,
69
+ type="NAV",
70
+ event="GO",
71
+ params=params,
72
+ session_id=session_id,
73
+ )
74
+
75
+ def open_chat(self, user_id: int, session_id: int) -> TelemetryEvent:
76
+ messages = self.rng.randint(60, 240)
77
+ render = self.rng.randint(50, 260)
78
+ duration = messages + render
79
+ return TelemetryEvent(
80
+ time=now_ms(),
81
+ user_id=user_id,
82
+ type="PERF",
83
+ event="open_chat_to_render",
84
+ params={
85
+ "spans": [
86
+ {
87
+ "duration": duration,
88
+ "name": "open_chat_to_render",
89
+ },
90
+ {
91
+ "duration": messages,
92
+ "name": "messages_list_created",
93
+ },
94
+ {
95
+ "duration": render,
96
+ "name": "messages_render",
97
+ },
98
+ ],
99
+ "properties": {
100
+ "class": 2,
101
+ "warm": 1,
102
+ "flow": 1,
103
+ },
104
+ },
105
+ session_id=session_id,
106
+ )
107
+
108
+ def open_chats(self, user_id: int, session_id: int) -> TelemetryEvent:
109
+ created = self.rng.randint(50, 230)
110
+ rendered = self.rng.randint(180, 650)
111
+ duration = created + rendered
112
+ return TelemetryEvent(
113
+ time=now_ms(),
114
+ user_id=user_id,
115
+ type="PERF",
116
+ event="open_chats_to_render",
117
+ params={
118
+ "spans": [
119
+ {
120
+ "duration": duration,
121
+ "name": "open_chats_to_render",
122
+ },
123
+ {
124
+ "duration": created,
125
+ "name": "chats_tab_created",
126
+ },
127
+ {
128
+ "duration": rendered,
129
+ "name": "chat_list_render",
130
+ },
131
+ ],
132
+ "properties": {"class": 2},
133
+ },
134
+ session_id=session_id,
135
+ )
136
+
137
+ def to_payload(self, events: list[TelemetryEvent]) -> dict:
138
+ return TelemetryPayload(events=events).to_payload()
139
+
140
+
141
+ def now_ms() -> int:
142
+ return int(time.time() * 1000)
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from contextlib import suppress
5
+ from random import Random
6
+ from typing import TYPE_CHECKING
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from pymax.logging import get_logger
11
+ from pymax.protocol import Opcode
12
+
13
+ from .navigation import (
14
+ MAIN_TAB_PARAMS,
15
+ NavigationPlanner,
16
+ RouteProfile,
17
+ Screen,
18
+ )
19
+ from .payloads import TelemetryEvent, TelemetryPayloadBuilder, now_ms
20
+
21
+ if TYPE_CHECKING:
22
+ from pymax.app import App
23
+ from pymax.types.domain import Chat
24
+
25
+
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ class TelemetryTiming(BaseModel):
30
+ model_config = {"frozen": True}
31
+
32
+ startup_delay: tuple[float, float] = (15.0, 90.0)
33
+ session_idle_delay: tuple[float, float] = (900.0, 2700.0)
34
+ render_delay: tuple[float, float] = (0.15, 1.2)
35
+ return_delay: tuple[float, float] = (12.0, 45.0)
36
+ return_to_background_chance: float = 0.40
37
+ open_chats_render_chance: float = 0.20
38
+
39
+
40
+ DEFAULT_TIMING = TelemetryTiming()
41
+
42
+
43
+ class TelemetryService:
44
+ def __init__(self, app: App) -> None:
45
+ self.app = app
46
+ self._rng = Random()
47
+ self._timing = DEFAULT_TIMING
48
+ self._planner = NavigationPlanner(self._rng)
49
+ self._payloads = TelemetryPayloadBuilder(self._rng)
50
+ self._task: asyncio.Task[None] | None = None
51
+ self._action_id = 0
52
+ self._session_id = app.config.device.client_session_id
53
+ self._last_nav_time = now_ms()
54
+
55
+ def start(self) -> None:
56
+ if self._task and not self._task.done():
57
+ logger.debug("telemetry start skipped: already running")
58
+ return
59
+
60
+ if not self._ready:
61
+ logger.debug("telemetry start skipped: app is not ready")
62
+ return
63
+
64
+ self._task = asyncio.create_task(
65
+ self._run(),
66
+ name="pymax.telemetry",
67
+ )
68
+ logger.debug("telemetry started")
69
+
70
+ async def stop(self) -> None:
71
+ if not self._task:
72
+ return
73
+
74
+ task = self._task
75
+ self._task = None
76
+ if not task.done():
77
+ task.cancel()
78
+ with suppress(asyncio.CancelledError):
79
+ await task
80
+ logger.debug("telemetry stopped")
81
+
82
+ async def _run(self) -> None:
83
+ try:
84
+ await asyncio.sleep(self._between(self._timing.startup_delay))
85
+ await self._send_events(
86
+ [self._payloads.login(self._user_id, self._session_id)]
87
+ )
88
+
89
+ while True:
90
+ self._session_id += 1
91
+ events = await self._collect_session_events(self._planner.new_profile())
92
+ await self._send_events(events)
93
+ self._planner.reset_to_background()
94
+ await asyncio.sleep(self._between(self._timing.session_idle_delay))
95
+
96
+ except asyncio.CancelledError:
97
+ raise
98
+ except Exception:
99
+ logger.debug("telemetry loop stopped by error", exc_info=True)
100
+ finally:
101
+ self._task = None
102
+
103
+ async def _collect_session_events(
104
+ self,
105
+ profile: RouteProfile,
106
+ ) -> list[TelemetryEvent]:
107
+ events: list[TelemetryEvent] = []
108
+
109
+ for _ in range(profile.steps):
110
+ await asyncio.sleep(profile.pause(self._rng))
111
+ if not self._ready:
112
+ return events
113
+
114
+ screen_from = self._planner.current_screen
115
+ screen_to = self._planner.next_screen(profile)
116
+ events.append(self._nav_event(screen_from, screen_to))
117
+
118
+ render_events = await self._render_events(screen_to)
119
+ events.extend(render_events)
120
+
121
+ if (
122
+ self._planner.current_screen != Screen.BACKGROUND
123
+ and self._rng.random() < self._timing.return_to_background_chance
124
+ ):
125
+ await asyncio.sleep(self._between(self._timing.return_delay))
126
+ screen_from = self._planner.current_screen
127
+ self._planner.reset_to_background()
128
+ events.append(self._nav_event(screen_from, Screen.BACKGROUND))
129
+
130
+ return events
131
+
132
+ async def _render_events(self, screen_to: Screen) -> list[TelemetryEvent]:
133
+ if screen_to == Screen.CHAT:
134
+ await asyncio.sleep(self._between(self._timing.render_delay))
135
+ return [self._payloads.open_chat(self._user_id, self._session_id)]
136
+
137
+ if (
138
+ screen_to == Screen.CHATS
139
+ and self._rng.random() < self._timing.open_chats_render_chance
140
+ ):
141
+ await asyncio.sleep(self._between(self._timing.render_delay))
142
+ return [self._payloads.open_chats(self._user_id, self._session_id)]
143
+
144
+ return []
145
+
146
+ async def _send_events(self, events: list[TelemetryEvent]) -> None:
147
+ if not events or not self._ready:
148
+ return
149
+
150
+ try:
151
+ await self.app.invoke(
152
+ Opcode.LOG,
153
+ self._payloads.to_payload(events),
154
+ timeout=self.app.config.request_timeout,
155
+ )
156
+ logger.debug("telemetry sent events=%s", len(events))
157
+ except asyncio.CancelledError:
158
+ raise
159
+ except Exception:
160
+ logger.debug("telemetry send failed", exc_info=True)
161
+
162
+ def _nav_event(self, screen_from: Screen, screen_to: Screen) -> TelemetryEvent:
163
+ event = self._payloads.navigation(
164
+ user_id=self._user_id,
165
+ session_id=self._session_id,
166
+ screen_from=int(screen_from),
167
+ screen_to=int(screen_to),
168
+ prev_time=self._last_nav_time,
169
+ action_id=self._next_action_id(),
170
+ extra_params=self._source_params(screen_to),
171
+ )
172
+ self._last_nav_time = event.time
173
+ return event
174
+
175
+ def _source_params(self, screen_to: Screen) -> dict[str, int]:
176
+ if screen_to == Screen.CHATS:
177
+ return dict(MAIN_TAB_PARAMS)
178
+
179
+ if screen_to != Screen.CHAT:
180
+ return {}
181
+
182
+ chat = self._pick_chat()
183
+ if chat is None:
184
+ return {
185
+ "source_type": 1,
186
+ "source_id": self._user_id,
187
+ }
188
+
189
+ return {
190
+ "source_type": _chat_source_type(chat),
191
+ "source_id": chat.id,
192
+ }
193
+
194
+ def _pick_chat(self) -> Chat | None:
195
+ chats = self.app.chats or []
196
+ if not chats:
197
+ return None
198
+ return self._rng.choice(chats)
199
+
200
+ def _next_action_id(self) -> int:
201
+ self._action_id = (self._action_id + 1) % 0xFFFFFFFF
202
+ return self._action_id
203
+
204
+ def _between(self, value: tuple[float, float]) -> float:
205
+ return self._rng.uniform(*value)
206
+
207
+ @property
208
+ def _ready(self) -> bool:
209
+ return (
210
+ self.app.started
211
+ and self.app.me is not None
212
+ and self.app.connection.is_open
213
+ )
214
+
215
+ @property
216
+ def _user_id(self) -> int:
217
+ if self.app.me is None:
218
+ raise RuntimeError("Telemetry requires authenticated profile")
219
+ return self.app.me.contact.id
220
+
221
+
222
+ def _chat_source_type(chat: Chat) -> int:
223
+ if str(chat.type) == "ChatType.DIALOG" or chat.type == "DIALOG":
224
+ return 1
225
+ return 2
File without changes
@@ -0,0 +1,14 @@
1
+ from typing import Protocol, overload
2
+
3
+
4
+ class Transport(Protocol):
5
+ async def connect(self) -> None: ...
6
+ async def close(self) -> None: ...
7
+ async def send(self, data: bytes | str) -> None: ...
8
+ @overload
9
+ async def recv(self) -> bytes | str: ...
10
+ @overload
11
+ async def recv(self, n: int) -> bytes | str: ...
12
+ async def recv(self, n: int | None = None) -> bytes | str: ...
13
+ @property
14
+ def connected(self) -> bool: ...
pymax/transport/tcp.py ADDED
@@ -0,0 +1,93 @@
1
+ import asyncio
2
+
3
+ from python_socks.async_.asyncio import Proxy
4
+
5
+ from pymax.logging import get_logger
6
+
7
+ from .base import Transport
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class TCPTransport(Transport):
13
+ def __init__(self, host: str, port: int, proxy: str | None, use_ssl: bool = True) -> None:
14
+ self._host = host
15
+ self._port = port
16
+ self._proxy = proxy
17
+ self._use_ssl = use_ssl
18
+ self._reader = None
19
+ self._writer = None
20
+
21
+ async def connect(self) -> None:
22
+ logger.debug(
23
+ "tcp connect host=%s port=%s ssl=%s",
24
+ self._host,
25
+ self._port,
26
+ self._use_ssl,
27
+ )
28
+ if self._proxy:
29
+ logger.debug("tcp connecting via proxy %s", self._proxy)
30
+ proxy = Proxy.from_url(self._proxy)
31
+ sock = await proxy.connect(
32
+ dest_host=self._host,
33
+ dest_port=self._port,
34
+ ssl=self._use_ssl,
35
+ )
36
+ self._reader, self._writer = await asyncio.open_connection(
37
+ sock=sock, ssl=self._use_ssl
38
+ )
39
+ logger.info(
40
+ "tcp connected via proxy %s host=%s port=%s ssl=%s",
41
+ self._proxy,
42
+ self._host,
43
+ self._port,
44
+ self._use_ssl,
45
+ )
46
+ else:
47
+ self._reader, self._writer = await asyncio.open_connection(
48
+ self._host,
49
+ self._port,
50
+ ssl=self._use_ssl,
51
+ )
52
+ logger.info(
53
+ "tcp connected host=%s port=%s ssl=%s",
54
+ self._host,
55
+ self._port,
56
+ self._use_ssl,
57
+ )
58
+
59
+ async def close(self) -> None:
60
+ if self._writer:
61
+ logger.debug("tcp close")
62
+ self._writer.close()
63
+ await self._writer.wait_closed()
64
+ logger.debug("tcp closed")
65
+
66
+ async def send(self, data: bytes | str) -> None:
67
+ if isinstance(data, str):
68
+ data = data.encode()
69
+
70
+ if self._writer is None or not self.connected:
71
+ logger.warning("tcp send failed: transport is not connected")
72
+ raise ConnectionError("Not connected to the server")
73
+
74
+ logger.debug("tcp send bytes=%s", len(data))
75
+ self._writer.write(data)
76
+ await self._writer.drain()
77
+
78
+ async def recv(self, n: int | None = None) -> bytes:
79
+ if self._reader is None or not self.connected:
80
+ logger.warning("tcp recv failed: transport is not connected")
81
+ raise ConnectionError("Not connected to the server")
82
+
83
+ if n is None:
84
+ data = await self._reader.readexactly(1024)
85
+ else:
86
+ data = await self._reader.readexactly(n)
87
+
88
+ logger.debug("tcp recv bytes=%s requested=%s", len(data), n)
89
+ return data
90
+
91
+ @property
92
+ def connected(self) -> bool:
93
+ return bool(self._reader and not self._reader.at_eof())
@@ -0,0 +1,50 @@
1
+ import asyncio
2
+
3
+ from websockets import ClientConnection, Origin
4
+ from websockets.asyncio import client
5
+
6
+ from pymax.logging import get_logger
7
+
8
+ from .base import Transport
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ class WebSocketTransport(Transport):
14
+ def __init__(self, url: str, proxy: str | None) -> None:
15
+ self.url = url
16
+ self.proxy = proxy
17
+ self.ws: ClientConnection | None = None
18
+
19
+ async def connect(self) -> None:
20
+ if self.proxy:
21
+ self.ws = await client.connect(
22
+ self.url, origin=Origin("https://web.max.ru"), proxy=self.proxy
23
+ )
24
+ else:
25
+ self.ws = await client.connect(
26
+ self.url, origin=Origin("https://web.max.ru")
27
+ ) # TODO: origin should be configurable
28
+
29
+ async def close(self) -> None:
30
+ if self.ws:
31
+ ws = self.ws
32
+ await ws.close()
33
+ await ws.wait_closed()
34
+ self.ws = None
35
+
36
+ async def send(self, data: bytes | str) -> None:
37
+ if self.ws is None or not self.connected:
38
+ raise ConnectionError("Not connected to the server")
39
+ logger.debug("sending %s", data)
40
+ await self.ws.send(data)
41
+
42
+ async def recv(self, n: int | None = None) -> bytes | str:
43
+ if self.ws is None or not self.connected:
44
+ raise ConnectionError("Not connected to the server")
45
+
46
+ return await self.ws.recv(decode=True)
47
+
48
+ @property
49
+ def connected(self) -> bool:
50
+ return bool(self.ws and self.ws.close_code is None)
@@ -0,0 +1,2 @@
1
+ from .domain import *
2
+ from .events import *
@@ -0,0 +1,11 @@
1
+ from .attachments import *
2
+ from .chat import Chat
3
+ from .error import MaxApiError
4
+ from .folder import Folder, FolderList, FolderUpdate
5
+ from .login import LoginResponse
6
+ from .message import Message, ReactionCounter, ReactionInfo, ReadState
7
+ from .name import Name
8
+ from .profile import Profile
9
+ from .session import Session
10
+ from .sync import SyncOverrides, SyncState
11
+ from .user import User
@@ -0,0 +1,11 @@
1
+ from .audio import AudioAttachment
2
+ from .call import CallAttachment
3
+ from .contact import ContactAttachment
4
+ from .control import ControlAttachment
5
+ from .enums import AttachmentType
6
+ from .file import FileAttachment, FileRequest
7
+ from .keyboards import InlineKeyboardAttachment
8
+ from .photo import PhotoAttachment
9
+ from .share import ShareAttachment
10
+ from .sticker import StickerAttachment
11
+ from .video import VideoAttachment, VideoRequest
@@ -0,0 +1,35 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import Field
4
+
5
+ from pymax.types.domain.base import CamelModel
6
+
7
+ from .enums import AttachmentType, TranscriptionStatus
8
+
9
+
10
+ class AudioAttachment(CamelModel):
11
+ """Аудио-вложение сообщения.
12
+
13
+ :ivar duration: Длительность аудио.
14
+ :vartype duration: int
15
+ :ivar audio_id: ID аудио.
16
+ :vartype audio_id: int
17
+ :ivar wave: Данные waveform.
18
+ :vartype wave: str | None
19
+ :ivar transcription_status: Статус транскрибации.
20
+ :vartype transcription_status: TranscriptionStatus | None
21
+ :ivar url: URL аудио.
22
+ :vartype url: str | None
23
+ :ivar type: Тип вложения.
24
+ :vartype type: Literal[AttachmentType.AUDIO]
25
+ :ivar token: Токен аудио.
26
+ :vartype token: str | None
27
+ """
28
+
29
+ duration: int
30
+ audio_id: int
31
+ wave: str | None = None
32
+ transcription_status: TranscriptionStatus | None = None
33
+ url: str | None = None
34
+ type: Literal[AttachmentType.AUDIO] = Field(alias="_type")
35
+ token: str | None = None
@@ -0,0 +1,26 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import Field
4
+
5
+ from pymax.types.domain.base import CamelModel
6
+
7
+ from .enums import AttachmentType
8
+
9
+
10
+ class CallAttachment(CamelModel):
11
+ """Вложение звонка.
12
+
13
+ :ivar type: Тип вложения.
14
+ :vartype type: Literal[AttachmentType.CALL]
15
+ :ivar duration: Длительность звонка.
16
+ :vartype duration: int | None
17
+ :ivar conversation_id: ID звонка или конференции.
18
+ :vartype conversation_id: str | int | None
19
+ :ivar contact_ids: ID участников звонка.
20
+ :vartype contact_ids: list[int]
21
+ """
22
+
23
+ type: Literal[AttachmentType.CALL] = Field(alias="_type")
24
+ duration: int | None = None
25
+ conversation_id: str | int | None = None
26
+ contact_ids: list[int] = Field(default_factory=list)
@@ -0,0 +1,32 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import Field
4
+
5
+ from pymax.types.domain.base import CamelModel
6
+
7
+ from .enums import AttachmentType
8
+
9
+
10
+ class ContactAttachment(CamelModel):
11
+ """Контактное вложение сообщения.
12
+
13
+ :ivar contact_id: ID контакта.
14
+ :vartype contact_id: int
15
+ :ivar first_name: Имя контакта.
16
+ :vartype first_name: str | None
17
+ :ivar last_name: Фамилия контакта.
18
+ :vartype last_name: str | None
19
+ :ivar name: Отображаемое имя контакта.
20
+ :vartype name: str | None
21
+ :ivar photo_url: URL фотографии контакта.
22
+ :vartype photo_url: str | None
23
+ :ivar type: Тип вложения.
24
+ :vartype type: Literal[AttachmentType.CONTACT]
25
+ """
26
+
27
+ contact_id: int
28
+ first_name: str | None = None
29
+ last_name: str | None = None
30
+ name: str | None = None
31
+ photo_url: str | None = None
32
+ type: Literal[AttachmentType.CONTACT] = Field(alias="_type")
@@ -0,0 +1,20 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import Field
4
+
5
+ from pymax.types.domain.base import CamelModel
6
+
7
+ from .enums import AttachmentType
8
+
9
+
10
+ class ControlAttachment(CamelModel):
11
+ """Служебное вложение управления.
12
+
13
+ :ivar type: Тип вложения.
14
+ :vartype type: Literal[AttachmentType.CONTROL]
15
+ :ivar event: Событие управления.
16
+ :vartype event: str
17
+ """
18
+
19
+ type: Literal[AttachmentType.CONTROL] = Field(alias="_type")
20
+ event: str