maxapi-python 1.2.5__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.5.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/protocol/enums.py +180 -0
- 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.5.dist-info/METADATA +0 -202
- maxapi_python-1.2.5.dist-info/RECORD +0 -33
- pymax/core.py +0 -398
- 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 -558
- pymax/mixins/__init__.py +0 -40
- pymax/mixins/auth.py +0 -594
- 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 -306
- pymax/mixins/telemetry.py +0 -118
- pymax/mixins/user.py +0 -219
- pymax/mixins/websocket.py +0 -151
- pymax/models.py +0 -8
- pymax/navigation.py +0 -187
- pymax/payloads.py +0 -403
- pymax/protocols.py +0 -123
- pymax/static/constant.py +0 -96
- pymax/static/enum.py +0 -231
- pymax/types.py +0 -1220
- pymax/utils.py +0 -90
- {maxapi_python-1.2.5.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
pymax/mixins/telemetry.py
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import random
|
|
3
|
-
import time
|
|
4
|
-
|
|
5
|
-
from pymax.exceptions import Error, SocketNotConnectedError
|
|
6
|
-
from pymax.navigation import Navigation
|
|
7
|
-
from pymax.payloads import (
|
|
8
|
-
NavigationEventParams,
|
|
9
|
-
NavigationEventPayload,
|
|
10
|
-
NavigationPayload,
|
|
11
|
-
)
|
|
12
|
-
from pymax.protocols import ClientProtocol
|
|
13
|
-
from pymax.static.enum import Opcode
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class TelemetryMixin(ClientProtocol):
|
|
17
|
-
async def _send_navigation_event(self, events: list[NavigationEventPayload]) -> None:
|
|
18
|
-
try:
|
|
19
|
-
payload = NavigationPayload(events=events).model_dump(by_alias=True)
|
|
20
|
-
data = await self._send_and_wait(
|
|
21
|
-
opcode=Opcode.LOG,
|
|
22
|
-
payload=payload,
|
|
23
|
-
)
|
|
24
|
-
payload_data = data.get("payload", {})
|
|
25
|
-
if payload_data and payload_data.get("error"):
|
|
26
|
-
error = payload_data.get("error")
|
|
27
|
-
self.logger.error("Navigation event error: %s", error)
|
|
28
|
-
except Exception:
|
|
29
|
-
self.logger.warning("Failed to send navigation event", exc_info=True)
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
async def _send_cold_start(self) -> None:
|
|
33
|
-
if not self.me:
|
|
34
|
-
self.logger.error("Cannot send cold start, user not set")
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
payload = NavigationEventPayload(
|
|
38
|
-
event="COLD_START",
|
|
39
|
-
time=int(time.time() * 1000),
|
|
40
|
-
user_id=self.me.id,
|
|
41
|
-
params=NavigationEventParams(
|
|
42
|
-
action_id=self._action_id,
|
|
43
|
-
screen_to=Navigation.get_screen_id("chats_list_tab"),
|
|
44
|
-
screen_from=1,
|
|
45
|
-
source_id=1,
|
|
46
|
-
session_id=self._session_id,
|
|
47
|
-
),
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
self._action_id += 1
|
|
51
|
-
|
|
52
|
-
await self._send_navigation_event([payload])
|
|
53
|
-
|
|
54
|
-
async def _send_random_navigation(self) -> None:
|
|
55
|
-
if not self.me:
|
|
56
|
-
self.logger.error("Cannot send navigation event, user not set")
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
screen_from = self._current_screen
|
|
60
|
-
screen_to = Navigation.get_random_navigation(screen_from)
|
|
61
|
-
|
|
62
|
-
self._action_id += 1
|
|
63
|
-
self._current_screen = screen_to
|
|
64
|
-
|
|
65
|
-
payload = NavigationEventPayload(
|
|
66
|
-
event="NAV",
|
|
67
|
-
time=int(time.time() * 1000),
|
|
68
|
-
user_id=self.me.id,
|
|
69
|
-
params=NavigationEventParams(
|
|
70
|
-
action_id=self._action_id,
|
|
71
|
-
screen_from=Navigation.get_screen_id(screen_from),
|
|
72
|
-
screen_to=Navigation.get_screen_id(screen_to),
|
|
73
|
-
source_id=1,
|
|
74
|
-
session_id=self._session_id,
|
|
75
|
-
),
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
await self._send_navigation_event([payload])
|
|
79
|
-
|
|
80
|
-
def _get_random_sleep_time(self) -> int:
|
|
81
|
-
# TODO: вынести в статик
|
|
82
|
-
sleep_options = [
|
|
83
|
-
(1000, 3000),
|
|
84
|
-
(300, 1000),
|
|
85
|
-
(60, 300),
|
|
86
|
-
(5, 60),
|
|
87
|
-
(5, 20),
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
weights = [0.05, 0.10, 0.15, 0.20, 0.50]
|
|
91
|
-
|
|
92
|
-
low, high = random.choices( # nosec B311
|
|
93
|
-
sleep_options, weights=weights, k=1
|
|
94
|
-
)[0]
|
|
95
|
-
return random.randint(low, high) # nosec B311
|
|
96
|
-
|
|
97
|
-
async def _start(self) -> None:
|
|
98
|
-
if not self.is_connected:
|
|
99
|
-
self.logger.error("Cannot start telemetry, client not connected")
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
await self._send_cold_start()
|
|
103
|
-
|
|
104
|
-
try:
|
|
105
|
-
while self.is_connected:
|
|
106
|
-
try:
|
|
107
|
-
await self._send_random_navigation()
|
|
108
|
-
except SocketNotConnectedError:
|
|
109
|
-
self.logger.debug("Socket disconnected, exiting telemetry task")
|
|
110
|
-
break
|
|
111
|
-
except Exception:
|
|
112
|
-
self.logger.warning("Failed to send random navigation")
|
|
113
|
-
await asyncio.sleep(self._get_random_sleep_time())
|
|
114
|
-
|
|
115
|
-
except asyncio.CancelledError:
|
|
116
|
-
self.logger.debug("Telemetry task cancelled")
|
|
117
|
-
except Exception:
|
|
118
|
-
self.logger.warning("Telemetry task failed", exc_info=True)
|
pymax/mixins/user.py
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
from typing import Any, Literal
|
|
2
|
-
|
|
3
|
-
from pymax.exceptions import Error, ResponseError, ResponseStructureError
|
|
4
|
-
from pymax.payloads import (
|
|
5
|
-
ContactActionPayload,
|
|
6
|
-
FetchContactsPayload,
|
|
7
|
-
SearchByPhonePayload,
|
|
8
|
-
)
|
|
9
|
-
from pymax.protocols import ClientProtocol
|
|
10
|
-
from pymax.static.enum import ContactAction, Opcode
|
|
11
|
-
from pymax.types import Contact, Session, User
|
|
12
|
-
from pymax.utils import MixinsUtils
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class UserMixin(ClientProtocol):
|
|
16
|
-
def get_cached_user(self, user_id: int) -> User | None:
|
|
17
|
-
"""
|
|
18
|
-
Получает пользователя из кеша по его идентификатору.
|
|
19
|
-
|
|
20
|
-
Проверяет внутренний кеш пользователей и возвращает объект User
|
|
21
|
-
если пользователь был ранее загружен.
|
|
22
|
-
|
|
23
|
-
:param user_id: Идентификатор пользователя.
|
|
24
|
-
:type user_id: int
|
|
25
|
-
:return: Объект User из кеша или None, если пользователь не найден.
|
|
26
|
-
:rtype: User | None
|
|
27
|
-
"""
|
|
28
|
-
user = self._users.get(user_id)
|
|
29
|
-
self.logger.debug("get_cached_user id=%s hit=%s", user_id, bool(user))
|
|
30
|
-
return user
|
|
31
|
-
|
|
32
|
-
async def get_users(self, user_ids: list[int]) -> list[User]:
|
|
33
|
-
"""
|
|
34
|
-
Получает информацию о пользователях по их идентификаторам.
|
|
35
|
-
|
|
36
|
-
Метод использует внутренний кеш для избежания повторных запросов.
|
|
37
|
-
Если пользователь уже загружен, берется из кеша, иначе выполняется
|
|
38
|
-
сетевой запрос к серверу.
|
|
39
|
-
|
|
40
|
-
:param user_ids: Список идентификаторов пользователей.
|
|
41
|
-
:type user_ids: list[int]
|
|
42
|
-
:return: Список объектов User в порядке, соответствующем входному списку.
|
|
43
|
-
:rtype: list[User]
|
|
44
|
-
"""
|
|
45
|
-
self.logger.debug("get_users ids=%s", user_ids)
|
|
46
|
-
cached = {uid: self._users[uid] for uid in user_ids if uid in self._users}
|
|
47
|
-
missing_ids = [uid for uid in user_ids if uid not in self._users]
|
|
48
|
-
|
|
49
|
-
if missing_ids:
|
|
50
|
-
self.logger.debug("Fetching missing users: %s", missing_ids)
|
|
51
|
-
fetched_users = await self.fetch_users(missing_ids)
|
|
52
|
-
if fetched_users:
|
|
53
|
-
for user in fetched_users:
|
|
54
|
-
self._users[user.id] = user
|
|
55
|
-
cached[user.id] = user
|
|
56
|
-
|
|
57
|
-
ordered = [cached[uid] for uid in user_ids if uid in cached]
|
|
58
|
-
self.logger.debug("get_users result_count=%d", len(ordered))
|
|
59
|
-
return ordered
|
|
60
|
-
|
|
61
|
-
async def get_user(self, user_id: int) -> User | None:
|
|
62
|
-
"""
|
|
63
|
-
Получает информацию о пользователе по его идентификатору.
|
|
64
|
-
|
|
65
|
-
Метод использует внутренний кеш. Если пользователь уже загружен,
|
|
66
|
-
возвращает его из кеша, иначе выполняет запрос к серверу.
|
|
67
|
-
|
|
68
|
-
:param user_id: Идентификатор пользователя.
|
|
69
|
-
:type user_id: int
|
|
70
|
-
:return: Объект User или None, если пользователь не найден.
|
|
71
|
-
:rtype: User | None
|
|
72
|
-
"""
|
|
73
|
-
self.logger.debug("get_user id=%s", user_id)
|
|
74
|
-
if user_id in self._users:
|
|
75
|
-
return self._users[user_id]
|
|
76
|
-
|
|
77
|
-
users = await self.fetch_users([user_id])
|
|
78
|
-
if users:
|
|
79
|
-
self._users[user_id] = users[0]
|
|
80
|
-
return users[0]
|
|
81
|
-
return None
|
|
82
|
-
|
|
83
|
-
async def fetch_users(self, user_ids: list[int]) -> list[User]:
|
|
84
|
-
"""
|
|
85
|
-
Загружает информацию о пользователях с сервера.
|
|
86
|
-
|
|
87
|
-
Запрашивает данные о пользователях по их идентификаторам и добавляет
|
|
88
|
-
их в внутренний кеш.
|
|
89
|
-
|
|
90
|
-
:param user_ids: Список идентификаторов пользователей для загрузки.
|
|
91
|
-
:type user_ids: list[int]
|
|
92
|
-
:return: Список загруженных объектов User.
|
|
93
|
-
:rtype: list[User]
|
|
94
|
-
"""
|
|
95
|
-
self.logger.info("Fetching users count=%d", len(user_ids))
|
|
96
|
-
|
|
97
|
-
payload = FetchContactsPayload(contact_ids=user_ids).model_dump(by_alias=True)
|
|
98
|
-
|
|
99
|
-
data = await self._send_and_wait(opcode=Opcode.CONTACT_INFO, payload=payload)
|
|
100
|
-
|
|
101
|
-
if data.get("payload", {}).get("error"):
|
|
102
|
-
MixinsUtils.handle_error(data)
|
|
103
|
-
|
|
104
|
-
users = [User.from_dict(u) for u in data["payload"].get("contacts", [])]
|
|
105
|
-
for user in users:
|
|
106
|
-
self._users[user.id] = user
|
|
107
|
-
|
|
108
|
-
self.logger.debug("Fetched users: %d", len(users))
|
|
109
|
-
return users
|
|
110
|
-
|
|
111
|
-
async def search_by_phone(self, phone: str) -> User:
|
|
112
|
-
"""
|
|
113
|
-
Выполняет поиск пользователя по номеру телефона.
|
|
114
|
-
|
|
115
|
-
:param phone: Номер телефона пользователя.
|
|
116
|
-
:type phone: str
|
|
117
|
-
:return: Объект User с найденными данными пользователя.
|
|
118
|
-
:rtype: User
|
|
119
|
-
:raises Error: Если пользователь не найден или произошла ошибка.
|
|
120
|
-
"""
|
|
121
|
-
self.logger.info("Searching user by phone: %s", phone)
|
|
122
|
-
|
|
123
|
-
payload = SearchByPhonePayload(phone=phone).model_dump(by_alias=True)
|
|
124
|
-
|
|
125
|
-
data = await self._send_and_wait(opcode=Opcode.CONTACT_INFO_BY_PHONE, payload=payload)
|
|
126
|
-
|
|
127
|
-
if data.get("payload", {}).get("error"):
|
|
128
|
-
MixinsUtils.handle_error(data)
|
|
129
|
-
|
|
130
|
-
if not data.get("payload"):
|
|
131
|
-
raise Error("no_payload", "No payload in response", "User Error")
|
|
132
|
-
|
|
133
|
-
user = User.from_dict(data["payload"]["contact"])
|
|
134
|
-
if not user:
|
|
135
|
-
raise Error("no_user", "User data missing in response", "User Error")
|
|
136
|
-
|
|
137
|
-
self._users[user.id] = user
|
|
138
|
-
self.logger.debug("Found user by phone: %s", user)
|
|
139
|
-
return user
|
|
140
|
-
|
|
141
|
-
async def get_sessions(self) -> list[Session]:
|
|
142
|
-
"""
|
|
143
|
-
Получает информацию о всех активных сессиях пользователя.
|
|
144
|
-
|
|
145
|
-
Возвращает список всех сессий, в которых авторизован пользователь.
|
|
146
|
-
|
|
147
|
-
:return: Список объектов Session.
|
|
148
|
-
:rtype: list[Session]
|
|
149
|
-
:raises Error: Если произошла ошибка при получении данных.
|
|
150
|
-
"""
|
|
151
|
-
self.logger.info("Fetching sessions")
|
|
152
|
-
|
|
153
|
-
data = await self._send_and_wait(opcode=Opcode.SESSIONS_INFO, payload={})
|
|
154
|
-
|
|
155
|
-
if data.get("payload", {}).get("error"):
|
|
156
|
-
MixinsUtils.handle_error(data)
|
|
157
|
-
|
|
158
|
-
if not data.get("payload"):
|
|
159
|
-
raise Error("no_payload", "No payload in response", "Session Error")
|
|
160
|
-
|
|
161
|
-
return [Session.from_dict(s) for s in data["payload"].get("sessions", [])]
|
|
162
|
-
|
|
163
|
-
async def _contact_action(self, payload: ContactActionPayload) -> dict[str, Any]:
|
|
164
|
-
data = await self._send_and_wait(
|
|
165
|
-
opcode=Opcode.CONTACT_UPDATE, # 34
|
|
166
|
-
payload=payload.model_dump(by_alias=True),
|
|
167
|
-
)
|
|
168
|
-
response_payload = data.get("payload")
|
|
169
|
-
if not isinstance(response_payload, dict):
|
|
170
|
-
raise ResponseStructureError("Invalid response structure")
|
|
171
|
-
if error := response_payload.get("error"):
|
|
172
|
-
raise ResponseError(error)
|
|
173
|
-
return response_payload
|
|
174
|
-
|
|
175
|
-
async def add_contact(self, contact_id: int) -> Contact:
|
|
176
|
-
"""
|
|
177
|
-
Добавляет контакт в список контактов
|
|
178
|
-
|
|
179
|
-
:param contact_id: ID контакта
|
|
180
|
-
:type contact_id: int
|
|
181
|
-
:return: Объект контакта
|
|
182
|
-
:rtype: Contact
|
|
183
|
-
:raises ResponseStructureError: Если структура ответа неверна
|
|
184
|
-
"""
|
|
185
|
-
payload = await self._contact_action(
|
|
186
|
-
ContactActionPayload(contact_id=contact_id, action=ContactAction.ADD)
|
|
187
|
-
)
|
|
188
|
-
contact_dict = payload.get("contact")
|
|
189
|
-
if isinstance(contact_dict, dict):
|
|
190
|
-
return Contact.from_dict(contact_dict)
|
|
191
|
-
raise ResponseStructureError("Wrong contact structure in response")
|
|
192
|
-
|
|
193
|
-
async def remove_contact(self, contact_id: int) -> Literal[True]:
|
|
194
|
-
"""
|
|
195
|
-
Удаляет контакт из списка контактов
|
|
196
|
-
|
|
197
|
-
:param contact_id: ID контакта
|
|
198
|
-
:type contact_id: int
|
|
199
|
-
:return: True если успешно
|
|
200
|
-
:rtype: Literal[True]
|
|
201
|
-
:raises ResponseStructureError: Если структура ответа неверна
|
|
202
|
-
"""
|
|
203
|
-
await self._contact_action(
|
|
204
|
-
ContactActionPayload(contact_id=contact_id, action=ContactAction.REMOVE)
|
|
205
|
-
)
|
|
206
|
-
return True
|
|
207
|
-
|
|
208
|
-
def get_chat_id(self, first_user_id: int, second_user_id: int) -> int:
|
|
209
|
-
"""
|
|
210
|
-
Получение айди лс (диалога)
|
|
211
|
-
|
|
212
|
-
:param first_user_id: ID первого пользователя
|
|
213
|
-
:type first_user_id: int
|
|
214
|
-
:param second_user_id: ID второго пользователя
|
|
215
|
-
:type second_user_id: int
|
|
216
|
-
:return: Айди диалога
|
|
217
|
-
:rtype: int
|
|
218
|
-
"""
|
|
219
|
-
return first_user_id ^ second_user_id
|
pymax/mixins/websocket.py
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import json
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
import websockets
|
|
6
|
-
from typing_extensions import override
|
|
7
|
-
|
|
8
|
-
from pymax.exceptions import WebSocketNotConnectedError
|
|
9
|
-
from pymax.interfaces import BaseTransport
|
|
10
|
-
from pymax.payloads import UserAgentPayload
|
|
11
|
-
from pymax.static.constant import (
|
|
12
|
-
DEFAULT_TIMEOUT,
|
|
13
|
-
RECV_LOOP_BACKOFF_DELAY,
|
|
14
|
-
WEBSOCKET_ORIGIN,
|
|
15
|
-
)
|
|
16
|
-
from pymax.static.enum import Opcode
|
|
17
|
-
from pymax.types import (
|
|
18
|
-
Chat,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class WebSocketMixin(BaseTransport):
|
|
23
|
-
@property
|
|
24
|
-
def ws(self) -> websockets.ClientConnection:
|
|
25
|
-
if self._ws is None or not self.is_connected:
|
|
26
|
-
self.logger.critical("WebSocket not connected when access attempted")
|
|
27
|
-
raise WebSocketNotConnectedError
|
|
28
|
-
return self._ws
|
|
29
|
-
|
|
30
|
-
async def connect(self, user_agent: UserAgentPayload | None = None) -> dict[str, Any] | None:
|
|
31
|
-
"""
|
|
32
|
-
Устанавливает соединение WebSocket с сервером и выполняет handshake.
|
|
33
|
-
|
|
34
|
-
:param user_agent: Пользовательский агент для handshake. Если None, используется значение по умолчанию.
|
|
35
|
-
:type user_agent: UserAgentPayload | None
|
|
36
|
-
:return: Результат handshake.
|
|
37
|
-
:rtype: dict[str, Any] | None
|
|
38
|
-
"""
|
|
39
|
-
if user_agent is None:
|
|
40
|
-
user_agent = UserAgentPayload()
|
|
41
|
-
|
|
42
|
-
self.logger.info("Connecting to WebSocket %s", self.uri)
|
|
43
|
-
|
|
44
|
-
if self._ws is not None or self.is_connected:
|
|
45
|
-
self.logger.warning("WebSocket already connected")
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
self._ws = await websockets.connect(
|
|
49
|
-
self.uri,
|
|
50
|
-
origin=WEBSOCKET_ORIGIN,
|
|
51
|
-
user_agent_header=user_agent.header_user_agent,
|
|
52
|
-
proxy=self.proxy,
|
|
53
|
-
)
|
|
54
|
-
self.is_connected = True
|
|
55
|
-
self._incoming = asyncio.Queue()
|
|
56
|
-
self._outgoing = asyncio.Queue()
|
|
57
|
-
self._pending = {}
|
|
58
|
-
self._recv_task = asyncio.create_task(self._recv_loop())
|
|
59
|
-
self._outgoing_task = asyncio.create_task(self._outgoing_loop())
|
|
60
|
-
self.logger.info("WebSocket connected, starting handshake")
|
|
61
|
-
return await self._handshake(user_agent)
|
|
62
|
-
|
|
63
|
-
async def _recv_loop(self) -> None:
|
|
64
|
-
if self._ws is None:
|
|
65
|
-
self.logger.warning("Recv loop started without websocket instance")
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
self.logger.debug("Receive loop started")
|
|
69
|
-
while True:
|
|
70
|
-
try:
|
|
71
|
-
raw = await self._ws.recv()
|
|
72
|
-
data = self._parse_json(raw)
|
|
73
|
-
|
|
74
|
-
if data is None:
|
|
75
|
-
continue
|
|
76
|
-
|
|
77
|
-
seq = data.get("seq")
|
|
78
|
-
if self._handle_pending(seq, data):
|
|
79
|
-
continue
|
|
80
|
-
|
|
81
|
-
await self._handle_incoming_queue(data)
|
|
82
|
-
await self._dispatch_incoming(data)
|
|
83
|
-
|
|
84
|
-
except websockets.exceptions.ConnectionClosed as e:
|
|
85
|
-
self.logger.info(
|
|
86
|
-
f"WebSocket connection closed with error: {e.code}, {e.reason}; exiting recv loop"
|
|
87
|
-
)
|
|
88
|
-
for fut in self._pending.values():
|
|
89
|
-
if not fut.done():
|
|
90
|
-
fut.set_exception(WebSocketNotConnectedError)
|
|
91
|
-
self._pending.clear()
|
|
92
|
-
|
|
93
|
-
self.is_connected = False
|
|
94
|
-
self._ws = None
|
|
95
|
-
self._recv_task = None
|
|
96
|
-
|
|
97
|
-
break
|
|
98
|
-
except Exception:
|
|
99
|
-
self.logger.exception("Error in recv_loop; backing off briefly")
|
|
100
|
-
await asyncio.sleep(RECV_LOOP_BACKOFF_DELAY)
|
|
101
|
-
|
|
102
|
-
@override
|
|
103
|
-
async def _send_and_wait(
|
|
104
|
-
self,
|
|
105
|
-
opcode: Opcode,
|
|
106
|
-
payload: dict[str, Any],
|
|
107
|
-
cmd: int = 0,
|
|
108
|
-
timeout: float = DEFAULT_TIMEOUT,
|
|
109
|
-
) -> dict[str, Any]:
|
|
110
|
-
ws = self.ws
|
|
111
|
-
msg = self._make_message(opcode, payload, cmd)
|
|
112
|
-
loop = asyncio.get_running_loop()
|
|
113
|
-
fut: asyncio.Future[dict[str, Any]] = loop.create_future()
|
|
114
|
-
seq_key = msg["seq"]
|
|
115
|
-
|
|
116
|
-
old_fut = self._pending.get(seq_key)
|
|
117
|
-
if old_fut and not old_fut.done():
|
|
118
|
-
old_fut.cancel()
|
|
119
|
-
|
|
120
|
-
self._pending[seq_key] = fut
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
self.logger.debug(
|
|
124
|
-
"Sending frame opcode=%s cmd=%s seq=%s",
|
|
125
|
-
opcode,
|
|
126
|
-
cmd,
|
|
127
|
-
msg["seq"],
|
|
128
|
-
)
|
|
129
|
-
await ws.send(json.dumps(msg))
|
|
130
|
-
data = await asyncio.wait_for(fut, timeout=timeout)
|
|
131
|
-
self.logger.debug(
|
|
132
|
-
"Received frame for seq=%s opcode=%s",
|
|
133
|
-
data.get("seq"),
|
|
134
|
-
data.get("opcode"),
|
|
135
|
-
)
|
|
136
|
-
return data
|
|
137
|
-
except asyncio.TimeoutError:
|
|
138
|
-
self.logger.exception("Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"])
|
|
139
|
-
raise RuntimeError("Send and wait failed")
|
|
140
|
-
except Exception:
|
|
141
|
-
self.logger.exception("Send and wait failed (opcode=%s, seq=%s)", opcode, msg["seq"])
|
|
142
|
-
raise RuntimeError("Send and wait failed")
|
|
143
|
-
finally:
|
|
144
|
-
self._pending.pop(seq_key, None)
|
|
145
|
-
|
|
146
|
-
@override
|
|
147
|
-
async def _get_chat(self, chat_id: int) -> Chat | None:
|
|
148
|
-
for chat in self.chats:
|
|
149
|
-
if chat.id == chat_id:
|
|
150
|
-
return chat
|
|
151
|
-
return None
|
pymax/models.py
DELETED
pymax/navigation.py
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import random
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Navigation:
|
|
5
|
-
SCREENS_GRAPH = { # noqa: RUF012
|
|
6
|
-
"chats_list_tab": [
|
|
7
|
-
"chat",
|
|
8
|
-
"contacts_tab",
|
|
9
|
-
"call_history_tab",
|
|
10
|
-
"settings_tab",
|
|
11
|
-
"create_chat",
|
|
12
|
-
"chat_attachments_voices",
|
|
13
|
-
],
|
|
14
|
-
"chat": [
|
|
15
|
-
"chats_list_tab",
|
|
16
|
-
"chat_attachments_media",
|
|
17
|
-
],
|
|
18
|
-
"contacts_tab": [
|
|
19
|
-
"call_history_tab",
|
|
20
|
-
"chats_list_tab",
|
|
21
|
-
"settings_tab",
|
|
22
|
-
"create_chat",
|
|
23
|
-
],
|
|
24
|
-
"call_history_tab": [
|
|
25
|
-
"chats_list_tab",
|
|
26
|
-
"settings_tab",
|
|
27
|
-
"contacts_tab",
|
|
28
|
-
],
|
|
29
|
-
"settings_tab": [
|
|
30
|
-
"settings_folders",
|
|
31
|
-
"settings_privacy",
|
|
32
|
-
"settings_notifications",
|
|
33
|
-
"settings_chat_decoration",
|
|
34
|
-
"call_history_tab",
|
|
35
|
-
"contacts_tab",
|
|
36
|
-
"chats_list_tab",
|
|
37
|
-
],
|
|
38
|
-
"settings_folders": [
|
|
39
|
-
"settings_tab",
|
|
40
|
-
"chats_list_tab",
|
|
41
|
-
"contacts_tab",
|
|
42
|
-
"call_history_tab",
|
|
43
|
-
],
|
|
44
|
-
"settings_privacy": [
|
|
45
|
-
"settings_tab",
|
|
46
|
-
"chats_list_tab",
|
|
47
|
-
"contacts_tab",
|
|
48
|
-
"call_history_tab",
|
|
49
|
-
],
|
|
50
|
-
"settings_notifications": [
|
|
51
|
-
"settings_tab",
|
|
52
|
-
"contacts_tab",
|
|
53
|
-
"call_history_tab",
|
|
54
|
-
"chats_list_tab",
|
|
55
|
-
],
|
|
56
|
-
"settings_chat_decoration": [
|
|
57
|
-
"settings_tab",
|
|
58
|
-
"chats_list_tab",
|
|
59
|
-
"contacts_tab",
|
|
60
|
-
"call_history_tab",
|
|
61
|
-
],
|
|
62
|
-
"create_chat": [
|
|
63
|
-
"chats_list_tab",
|
|
64
|
-
"contacts_tab",
|
|
65
|
-
],
|
|
66
|
-
"chat_attachments_media": [
|
|
67
|
-
"chat_attachments_files",
|
|
68
|
-
"chat_attachments_voices",
|
|
69
|
-
"chat_attachments_links",
|
|
70
|
-
"chat",
|
|
71
|
-
],
|
|
72
|
-
"chat_attachments_files": [
|
|
73
|
-
"chat_attachments_voices",
|
|
74
|
-
"chat_attachments_media",
|
|
75
|
-
"chat_attachments_links",
|
|
76
|
-
"chat",
|
|
77
|
-
],
|
|
78
|
-
"chat_attachments_voices": [
|
|
79
|
-
"chat_attachments_links",
|
|
80
|
-
"chat_attachments_media",
|
|
81
|
-
"chat_attachments_files",
|
|
82
|
-
"chat",
|
|
83
|
-
],
|
|
84
|
-
"chat_attachments_links": [
|
|
85
|
-
"chat_attachments_media",
|
|
86
|
-
"chat_attachments_files",
|
|
87
|
-
"chat_attachments_voices",
|
|
88
|
-
"chat",
|
|
89
|
-
],
|
|
90
|
-
}
|
|
91
|
-
SCREENS = { # noqa: RUF012
|
|
92
|
-
"application_background": 1,
|
|
93
|
-
"auth_sign_method": 50,
|
|
94
|
-
"auth_phone_login": 51,
|
|
95
|
-
"auth_otp": 52,
|
|
96
|
-
"auth_empty_profile": 53,
|
|
97
|
-
"auth_avatars": 54,
|
|
98
|
-
"contacts_tab": 100,
|
|
99
|
-
"contacts_search": 102,
|
|
100
|
-
"contacts_search_by_phone": 103,
|
|
101
|
-
"chats_list_tab": 150,
|
|
102
|
-
"chats_list_search_initial": 151,
|
|
103
|
-
"chats_list_search_result": 152,
|
|
104
|
-
"create_chat": 200,
|
|
105
|
-
"create_chat_members_picker": 201,
|
|
106
|
-
"create_chat_info": 202,
|
|
107
|
-
"avatar_picker_gallery": 250,
|
|
108
|
-
"avatar_picker_crop": 251,
|
|
109
|
-
"avatar_picker_camera": 252,
|
|
110
|
-
"avatar_viewer": 253,
|
|
111
|
-
"call_history_tab": 300,
|
|
112
|
-
"call_new_call": 302,
|
|
113
|
-
"call_create_group_link": 303,
|
|
114
|
-
"call_add_participants": 304,
|
|
115
|
-
"call": 305,
|
|
116
|
-
"chat": 350,
|
|
117
|
-
"chat_attach_picker": 351,
|
|
118
|
-
"chat_attach_picker_media_viewer": 352,
|
|
119
|
-
"chat_attach_picker_camera": 353,
|
|
120
|
-
"chat_share_location": 354,
|
|
121
|
-
"chat_share_contact": 355,
|
|
122
|
-
"chat_forward": 357,
|
|
123
|
-
"chat_media_viewer": 358,
|
|
124
|
-
"chat_system_file_viewer": 359,
|
|
125
|
-
"chat_location_viewer": 360,
|
|
126
|
-
"chat_info": 400,
|
|
127
|
-
"chat_info_all_participants": 401,
|
|
128
|
-
"chat_info_editing": 402,
|
|
129
|
-
"chat_info_add_participants": 403,
|
|
130
|
-
"chat_info_administrators": 404,
|
|
131
|
-
"chat_info_add_administrator": 405,
|
|
132
|
-
"chat_info_blocked_participants": 406,
|
|
133
|
-
"chat_info_change_owner": 407,
|
|
134
|
-
"chat_attachments_media": 408,
|
|
135
|
-
"chat_attachments_files": 409,
|
|
136
|
-
"chat_attachments_links": 410,
|
|
137
|
-
"chat_info_invite_link": 411,
|
|
138
|
-
"chat_attachments_voices": 412,
|
|
139
|
-
"settings_tab": 450,
|
|
140
|
-
"settings_profile_editing": 451,
|
|
141
|
-
"settings_shortname_change": 452,
|
|
142
|
-
"settings_phone_change": 453,
|
|
143
|
-
"settings_notifications": 454,
|
|
144
|
-
"settings_notifications_system": 455,
|
|
145
|
-
"settings_folders": 456,
|
|
146
|
-
"settings_privacy": 457,
|
|
147
|
-
"settings_privacy_block_list": 458,
|
|
148
|
-
"settings_media": 459,
|
|
149
|
-
"settings_messages": 460,
|
|
150
|
-
"settings_stickers": 461,
|
|
151
|
-
"settings_chat_decoration": 462,
|
|
152
|
-
"settings_phone_change_phone_input": 463,
|
|
153
|
-
"settings_phone_change_phone_otp": 464,
|
|
154
|
-
"settings_cache": 465,
|
|
155
|
-
"settings_profile_avatars": 466,
|
|
156
|
-
"settings_about_application": 467,
|
|
157
|
-
"settings_privacy_sensitive_content": 479,
|
|
158
|
-
"miniapp": 500,
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
@classmethod
|
|
162
|
-
def get_screen_id(cls, screen_name: str) -> int:
|
|
163
|
-
screen_id = cls.SCREENS.get(screen_name)
|
|
164
|
-
|
|
165
|
-
if screen_id is None:
|
|
166
|
-
raise ValueError(f"Unknown screen name: {screen_name}")
|
|
167
|
-
|
|
168
|
-
return screen_id
|
|
169
|
-
|
|
170
|
-
@classmethod
|
|
171
|
-
def can_navigate(cls, from_screen: str, to_screen: str) -> bool:
|
|
172
|
-
if from_screen == to_screen:
|
|
173
|
-
return True
|
|
174
|
-
return to_screen in cls.SCREENS_GRAPH.get(from_screen, [])
|
|
175
|
-
|
|
176
|
-
@classmethod
|
|
177
|
-
def get_random_navigation(cls, screen_name: str) -> str:
|
|
178
|
-
return random.choice( # nosec B311
|
|
179
|
-
cls.SCREENS_GRAPH.get(screen_name, [])
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
@classmethod
|
|
183
|
-
def get_screen_name(cls, screen_id: int) -> str | None:
|
|
184
|
-
for name, id_ in cls.SCREENS.items():
|
|
185
|
-
if id_ == screen_id:
|
|
186
|
-
return name
|
|
187
|
-
return None
|