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.
- maxapi_python-2.0.0.dist-info/METADATA +217 -0
- maxapi_python-2.0.0.dist-info/RECORD +140 -0
- {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +50 -105
- pymax/api/__init__.py +17 -0
- pymax/api/auth/__init__.py +1 -0
- pymax/api/auth/enums.py +17 -0
- pymax/api/auth/payloads.py +129 -0
- pymax/api/auth/service.py +313 -0
- pymax/api/auth/types.py +13 -0
- pymax/api/chats/__init__.py +8 -0
- pymax/api/chats/enums.py +27 -0
- pymax/api/chats/payloads.py +103 -0
- pymax/api/chats/service.py +277 -0
- pymax/api/facade.py +32 -0
- pymax/api/messages/__init__.py +1 -0
- pymax/api/messages/enums.py +17 -0
- pymax/api/messages/payloads.py +92 -0
- pymax/api/messages/service.py +337 -0
- pymax/api/models.py +13 -0
- pymax/api/response.py +123 -0
- pymax/api/self/__init__.py +2 -0
- pymax/api/self/enums.py +11 -0
- pymax/api/self/payloads.py +41 -0
- pymax/api/self/service.py +142 -0
- pymax/api/session/__init__.py +1 -0
- pymax/api/session/enums.py +10 -0
- pymax/api/session/payloads.py +76 -0
- pymax/api/session/service.py +72 -0
- pymax/api/uploads/__init__.py +1 -0
- pymax/api/uploads/models.py +49 -0
- pymax/api/uploads/payloads.py +25 -0
- pymax/api/uploads/service.py +458 -0
- pymax/api/users/__init__.py +2 -0
- pymax/api/users/enums.py +12 -0
- pymax/api/users/payloads.py +16 -0
- pymax/api/users/service.py +124 -0
- pymax/app.py +273 -0
- pymax/auth/__init__.py +25 -0
- pymax/auth/base.py +37 -0
- pymax/auth/email.py +0 -0
- pymax/auth/models.py +5 -0
- pymax/auth/providers.py +127 -0
- pymax/auth/qr.py +135 -0
- pymax/auth/service.py +25 -0
- pymax/auth/sms.py +122 -0
- pymax/base.py +204 -0
- pymax/client.py +106 -0
- pymax/client_web.py +83 -0
- pymax/config.py +215 -0
- pymax/connection/__init__.py +1 -0
- pymax/connection/connection.py +205 -0
- pymax/connection/pending.py +46 -0
- pymax/connection/readers/__init__.py +2 -0
- pymax/connection/readers/base.py +6 -0
- pymax/connection/readers/tcp.py +29 -0
- pymax/connection/readers/ws.py +14 -0
- pymax/dispatch/__init__.py +10 -0
- pymax/dispatch/dispatcher.py +222 -0
- pymax/dispatch/enums.py +12 -0
- pymax/dispatch/mapping.py +73 -0
- pymax/dispatch/resolvers.py +52 -0
- pymax/dispatch/router.py +216 -0
- pymax/exceptions.py +22 -89
- pymax/files/__init__.py +9 -0
- pymax/files/base.py +82 -0
- pymax/files/file.py +76 -0
- pymax/files/photo.py +108 -0
- pymax/files/static.py +10 -0
- pymax/files/video.py +74 -0
- pymax/formatting/__init__.py +0 -0
- pymax/formatting/markdown.py +217 -0
- pymax/infra/__init__.py +1 -0
- pymax/infra/auth.py +55 -0
- pymax/infra/base.py +15 -0
- pymax/infra/chat.py +240 -0
- pymax/infra/message.py +252 -0
- pymax/infra/protocol.py +9 -0
- pymax/infra/self.py +139 -0
- pymax/infra/user.py +107 -0
- pymax/logging.py +129 -0
- pymax/protocol/__init__.py +11 -0
- pymax/protocol/base.py +13 -0
- pymax/{static/enum.py → protocol/enums.py} +36 -79
- pymax/protocol/models.py +33 -0
- pymax/protocol/tcp/__init__.py +1 -0
- pymax/protocol/tcp/compression.py +97 -0
- pymax/protocol/tcp/framing.py +68 -0
- pymax/protocol/tcp/payload.py +127 -0
- pymax/protocol/tcp/protocol.py +68 -0
- pymax/protocol/ws/__init__.py +1 -0
- pymax/protocol/ws/protocol.py +27 -0
- pymax/py.typed +0 -0
- pymax/routers.py +8 -0
- pymax/session/__init__.py +3 -0
- pymax/session/models.py +11 -0
- pymax/session/protocol.py +14 -0
- pymax/session/store.py +232 -0
- pymax/telemetry/__init__.py +3 -0
- pymax/telemetry/navigation.py +181 -0
- pymax/telemetry/payloads.py +142 -0
- pymax/telemetry/service.py +225 -0
- pymax/transport/__init__.py +0 -0
- pymax/transport/base.py +14 -0
- pymax/transport/tcp.py +93 -0
- pymax/transport/websocket.py +50 -0
- pymax/types/__init__.py +2 -0
- pymax/types/domain/__init__.py +11 -0
- pymax/types/domain/attachments/__init__.py +11 -0
- pymax/types/domain/attachments/audio.py +35 -0
- pymax/types/domain/attachments/call.py +26 -0
- pymax/types/domain/attachments/contact.py +32 -0
- pymax/types/domain/attachments/control.py +20 -0
- pymax/types/domain/attachments/enums.py +27 -0
- pymax/types/domain/attachments/file.py +56 -0
- pymax/types/domain/attachments/keyboards/__init__.py +1 -0
- pymax/types/domain/attachments/keyboards/inline.py +19 -0
- pymax/types/domain/attachments/photo.py +45 -0
- pymax/types/domain/attachments/share.py +29 -0
- pymax/types/domain/attachments/sticker.py +50 -0
- pymax/types/domain/attachments/video.py +90 -0
- pymax/types/domain/auth.py +161 -0
- pymax/types/domain/base.py +17 -0
- pymax/types/domain/chat.py +426 -0
- pymax/types/domain/element.py +24 -0
- pymax/types/domain/enums.py +24 -0
- pymax/types/domain/error.py +20 -0
- pymax/types/domain/folder.py +74 -0
- pymax/types/domain/login.py +35 -0
- pymax/types/domain/message.py +378 -0
- pymax/types/domain/name.py +20 -0
- pymax/types/domain/profile.py +15 -0
- pymax/types/domain/session.py +52 -0
- pymax/types/domain/sync.py +80 -0
- pymax/types/domain/user.py +117 -0
- pymax/types/events/__init__.py +3 -0
- pymax/types/events/file.py +5 -0
- pymax/types/events/message.py +37 -0
- pymax/types/events/video.py +5 -0
- maxapi_python-1.2.4.dist-info/METADATA +0 -205
- maxapi_python-1.2.4.dist-info/RECORD +0 -33
- pymax/core.py +0 -390
- pymax/crud.py +0 -96
- pymax/files.py +0 -138
- pymax/filters.py +0 -164
- pymax/formatter.py +0 -31
- pymax/formatting.py +0 -74
- pymax/interfaces.py +0 -552
- pymax/mixins/__init__.py +0 -40
- pymax/mixins/auth.py +0 -368
- pymax/mixins/channel.py +0 -130
- pymax/mixins/group.py +0 -458
- pymax/mixins/handler.py +0 -285
- pymax/mixins/message.py +0 -879
- pymax/mixins/scheduler.py +0 -28
- pymax/mixins/self.py +0 -259
- pymax/mixins/socket.py +0 -297
- pymax/mixins/telemetry.py +0 -112
- pymax/mixins/user.py +0 -219
- pymax/mixins/websocket.py +0 -142
- pymax/models.py +0 -8
- pymax/navigation.py +0 -187
- pymax/payloads.py +0 -367
- pymax/protocols.py +0 -123
- pymax/static/constant.py +0 -89
- pymax/types.py +0 -1220
- pymax/utils.py +0 -90
- {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
|
pymax/transport/base.py
ADDED
|
@@ -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)
|
pymax/types/__init__.py
ADDED
|
@@ -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
|