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.
Files changed (169) 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.5.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/protocol/enums.py +180 -0
  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.5.dist-info/METADATA +0 -202
  141. maxapi_python-1.2.5.dist-info/RECORD +0 -33
  142. pymax/core.py +0 -398
  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 -558
  149. pymax/mixins/__init__.py +0 -40
  150. pymax/mixins/auth.py +0 -594
  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 -306
  158. pymax/mixins/telemetry.py +0 -118
  159. pymax/mixins/user.py +0 -219
  160. pymax/mixins/websocket.py +0 -151
  161. pymax/models.py +0 -8
  162. pymax/navigation.py +0 -187
  163. pymax/payloads.py +0 -403
  164. pymax/protocols.py +0 -123
  165. pymax/static/constant.py +0 -96
  166. pymax/static/enum.py +0 -231
  167. pymax/types.py +0 -1220
  168. pymax/utils.py +0 -90
  169. {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
@@ -1,8 +0,0 @@
1
- from uuid import UUID, uuid4
2
-
3
- from sqlmodel import Field, SQLModel
4
-
5
-
6
- class Auth(SQLModel, table=True):
7
- token: str | None = None
8
- device_id: UUID = Field(default_factory=uuid4, primary_key=True)
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