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/auth/qr.py ADDED
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from typing import TYPE_CHECKING
6
+
7
+ import qrcode
8
+
9
+ from pymax.exceptions import ApiError
10
+ from pymax.logging import get_logger
11
+
12
+ from .models import AuthResult
13
+ from .providers import ConsolePasswordProvider, PasswordProvider, QrHandler
14
+
15
+ if TYPE_CHECKING:
16
+ from pymax.app import App
17
+ from pymax.types.domain.auth import RequestQrResponse
18
+
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ class QrAuthFlow:
24
+ """Стандартная QR-авторизация ``WebClient``.
25
+
26
+ Flow получает QR-ссылку, передает ее в ``QrHandler``, ждет подтверждения и
27
+ возвращает token для сохранения в сессии.
28
+
29
+ Args:
30
+ qr_provider: Handler, который показывает QR пользователю.
31
+ password_provider: Provider пароля 2FA. Если не передан, используется
32
+ ``ConsolePasswordProvider``.
33
+
34
+ Example:
35
+ .. code-block:: python
36
+
37
+ from pymax import ConsoleQrHandler, QrAuthFlow, WebClient
38
+
39
+ flow = QrAuthFlow(ConsoleQrHandler())
40
+ client = WebClient(auth_flow=flow)
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ qr_provider: QrHandler,
46
+ password_provider: PasswordProvider | None = None,
47
+ ) -> None:
48
+ self.qr_provider = qr_provider
49
+ self.password_provider = password_provider or ConsolePasswordProvider()
50
+
51
+ async def authenticate(self, app: App) -> AuthResult:
52
+ """Проходит QR-авторизацию.
53
+
54
+ Args:
55
+ app: Внутренний runtime PyMax.
56
+
57
+ Returns:
58
+ ``AuthResult`` с token.
59
+
60
+ Raises:
61
+ RuntimeError: Если QR истек до подтверждения.
62
+ """
63
+ logger.info("starting qr authentication")
64
+
65
+ qr_info = await app.api.auth.request_qr()
66
+
67
+ logger.debug("got qr track_id=%s", qr_info.track_id)
68
+
69
+ await self.qr_provider.show_qr(qr_info.qr_link)
70
+
71
+ confirmed = await self._poll_qr(app, qr_info)
72
+
73
+ if not confirmed:
74
+ raise RuntimeError("QR authentication expired")
75
+
76
+ result = await app.api.auth.confirm_qr(qr_info.track_id)
77
+
78
+ token = result.login_token
79
+ if not token and result.password_challenge:
80
+ token = await self._authenticate_with_password(
81
+ app,
82
+ track_id=result.password_challenge.track_id,
83
+ hint=result.password_challenge.hint,
84
+ )
85
+
86
+ return AuthResult(
87
+ token=token,
88
+ )
89
+
90
+ async def _poll_qr(self, app: App, qr_info: RequestQrResponse) -> bool:
91
+ interval = qr_info.polling_interval / 1000
92
+ expires_at = qr_info.expires_at / 1000
93
+
94
+ while time.time() < expires_at:
95
+ response = await app.api.auth.check_qr(qr_info.track_id)
96
+
97
+ if response.status.login_available:
98
+ return True
99
+
100
+ await asyncio.sleep(interval)
101
+
102
+ return False
103
+
104
+ async def _authenticate_with_password(
105
+ self,
106
+ app: App,
107
+ track_id: str,
108
+ hint: str | None,
109
+ ) -> str:
110
+ logger.info("starting 2fa password authentication")
111
+ while True:
112
+ password = await self.password_provider.get_password(hint)
113
+ logger.debug(
114
+ "2fa password provider returned password_set=%s",
115
+ bool(password),
116
+ )
117
+ if not password:
118
+ logger.warning("2fa password is empty; retrying")
119
+ continue
120
+
121
+ try:
122
+ response = await app.api.auth.check_password(track_id, password)
123
+ except ApiError as e:
124
+ logger.error("2fa password check failed: %s", e)
125
+ continue
126
+
127
+ if response.error:
128
+ logger.error("2fa password check failed error=%s", response.error)
129
+ continue
130
+
131
+ if response.login_token:
132
+ logger.info("2fa password authentication completed")
133
+ return response.login_token
134
+
135
+ logger.error("2fa password response did not contain login token; retrying")
pymax/auth/service.py ADDED
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from pymax.logging import get_logger
6
+
7
+ from .providers import SmsCodeProvider
8
+ from .sms import SmsAuthFlow
9
+
10
+ if TYPE_CHECKING:
11
+ from pymax.app import App
12
+
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class AuthService:
18
+ def __init__(self, app: App, sms_code_provider: SmsCodeProvider) -> None:
19
+ self.app = app
20
+ self.sms_code_provider = sms_code_provider
21
+ self.sms = SmsAuthFlow(self.sms_code_provider)
22
+ logger.debug(
23
+ "auth service initialized sms_provider=%s",
24
+ type(sms_code_provider).__name__,
25
+ )
pymax/auth/sms.py ADDED
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from pymax.exceptions import ApiError
6
+ from pymax.logging import get_logger
7
+
8
+ from .models import AuthResult
9
+ from .providers import ConsolePasswordProvider, PasswordProvider, SmsCodeProvider
10
+
11
+ if TYPE_CHECKING:
12
+ from pymax.app import App
13
+
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class SmsAuthFlow:
19
+ """Стандартная SMS-авторизация ``Client``.
20
+
21
+ Flow запрашивает SMS-код, отправляет его в Max, при необходимости проходит
22
+ пароль 2FA и возвращает token для сохранения в сессии.
23
+
24
+ Args:
25
+ code_provider: Provider, который возвращает SMS-код.
26
+ password_provider: Provider пароля 2FA. Если не передан, используется
27
+ ``ConsolePasswordProvider``.
28
+
29
+ Example:
30
+ .. code-block:: python
31
+
32
+ from pymax import Client, ConsoleSmsCodeProvider, SmsAuthFlow
33
+
34
+ flow = SmsAuthFlow(ConsoleSmsCodeProvider())
35
+ client = Client(
36
+ phone="+79990000000",
37
+ auth_flow=flow,
38
+ )
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ code_provider: SmsCodeProvider,
44
+ password_provider: PasswordProvider | None = None,
45
+ ) -> None:
46
+ self.code_provider = code_provider
47
+ self.password_provider = password_provider or ConsolePasswordProvider()
48
+
49
+ async def authenticate(self, app: App) -> AuthResult:
50
+ """Проходит SMS/2FA-авторизацию.
51
+
52
+ Args:
53
+ app: Внутренний runtime PyMax.
54
+
55
+ Returns:
56
+ ``AuthResult`` с token.
57
+
58
+ Raises:
59
+ RuntimeError: Если у клиента нет телефона.
60
+ """
61
+ phone = app.config.phone
62
+ if not phone:
63
+ logger.error("sms authentication requested without phone")
64
+ raise RuntimeError("Phone is required for SMS authentication")
65
+
66
+ logger.info("starting sms authentication")
67
+ start = await app.api.auth.request_code(phone)
68
+ logger.debug("sms token received token_set=%s", bool(start.token))
69
+ code = await self.code_provider.get_code(phone)
70
+ logger.debug("sms code provider returned code_set=%s", bool(code))
71
+ result = await app.api.auth.send_code(start.token, code)
72
+
73
+ token = result.login_token
74
+ if not token and result.password_challenge:
75
+ token = await self._authenticate_with_password(
76
+ app,
77
+ track_id=result.password_challenge.track_id,
78
+ hint=result.password_challenge.hint,
79
+ )
80
+
81
+ logger.info(
82
+ "sms authentication completed token_set=%s",
83
+ bool(token),
84
+ )
85
+ return AuthResult(
86
+ token=token,
87
+ )
88
+
89
+ async def _authenticate_with_password(
90
+ self,
91
+ app: App,
92
+ track_id: str,
93
+ hint: str | None,
94
+ ) -> str:
95
+ logger.info("starting 2fa password authentication")
96
+ while True:
97
+ password = await self.password_provider.get_password(hint)
98
+ logger.debug(
99
+ "2fa password provider returned password_set=%s",
100
+ bool(password),
101
+ )
102
+ if not password:
103
+ logger.warning("2fa password is empty; retrying")
104
+ continue
105
+
106
+ try:
107
+ response = await app.api.auth.check_password(track_id, password)
108
+ except ApiError as e:
109
+ logger.error("2fa password check failed: %s", e)
110
+ continue
111
+
112
+ if response.error:
113
+ logger.error("2fa password check failed error=%s", response.error)
114
+ continue
115
+
116
+ if response.login_token:
117
+ logger.info("2fa password authentication completed")
118
+ return response.login_token
119
+
120
+ logger.error(
121
+ "2fa password response did not contain login token; retrying"
122
+ )
pymax/base.py ADDED
@@ -0,0 +1,204 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from abc import ABC, abstractmethod
5
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
+ from uuid import uuid4
7
+
8
+ from pymax.dispatch import Router
9
+ from pymax.infra import BaseMixin
10
+ from pymax.logging import get_logger
11
+
12
+ from .app import App
13
+ from .config import ClientConfig, DeviceConfig, ExtraConfig
14
+
15
+ if TYPE_CHECKING:
16
+ from pymax.api.session.payloads import MobileUserAgentPayload
17
+ from pymax.auth import AuthFlow
18
+ from pymax.connection import ConnectionManager
19
+ from pymax.dispatch.router import (
20
+ FilterCallback,
21
+ HandlerDecorator,
22
+ StartDecorator,
23
+ )
24
+ from pymax.protocol import InboundFrame
25
+ from pymax.types import Chat, MessageDeleteEvent, User
26
+ from pymax.types.domain import Message, Profile
27
+
28
+ logger = get_logger(__name__)
29
+ ClientT = TypeVar("ClientT", bound="BaseClient[Any]")
30
+
31
+
32
+ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
33
+ extra_config: ExtraConfig
34
+ session_name: str
35
+ work_dir: str
36
+ _config: ClientConfig
37
+ _connection: ConnectionManager
38
+ _auth_flow: AuthFlow
39
+ _router: Router[ClientT]
40
+ _app: App[ClientT]
41
+
42
+ @property
43
+ def me(self) -> Profile | None:
44
+ """Профиль текущего аккаунта после успешного ``start``."""
45
+ return self._app.me
46
+
47
+ @property
48
+ def chats(self) -> list[Chat] | None:
49
+ """Чаты, которые Max вернул на login/sync."""
50
+ return self._app.chats
51
+
52
+ @property
53
+ def contacts(self) -> list[User | None]:
54
+ """Контакты, которые Max вернул на login/sync."""
55
+ return self._app.contacts
56
+
57
+ @property
58
+ def messages(self) -> dict[int, list[Message]] | None:
59
+ """Сообщения, которые Max вернул на login/sync."""
60
+ return self._app.messages
61
+
62
+ def _build_config(
63
+ self,
64
+ *,
65
+ phone: str | None,
66
+ user_agent: MobileUserAgentPayload,
67
+ ) -> ClientConfig:
68
+ logger.debug(
69
+ "building client config token_set=%s",
70
+ bool(self.extra_config.token),
71
+ )
72
+ return ClientConfig(
73
+ phone=phone,
74
+ session_name=self.session_name,
75
+ work_dir=self.work_dir,
76
+ token=self.extra_config.token,
77
+ host=self.extra_config.host,
78
+ port=self.extra_config.port,
79
+ use_ssl=self.extra_config.use_ssl,
80
+ request_timeout=self.extra_config.request_timeout,
81
+ log_level=self.extra_config.log_level,
82
+ telemetry=self.extra_config.telemetry,
83
+ sync=self.extra_config.sync,
84
+ store=self.extra_config.store,
85
+ device=DeviceConfig(
86
+ mt_instance_id=self.extra_config.mt_instance_id,
87
+ device_id=self.extra_config.device_id or str(uuid4()),
88
+ user_agent=user_agent,
89
+ ),
90
+ )
91
+
92
+ @abstractmethod
93
+ def _build_connection(self) -> ConnectionManager:
94
+ raise NotImplementedError
95
+
96
+ def _init_runtime(self: ClientT, *, auth_flow: AuthFlow) -> None: # noqa: PYI019
97
+ self._connection = self._build_connection()
98
+ self._auth_flow = auth_flow
99
+ self._router = Router()
100
+ self._app = self._build_app()
101
+
102
+ def _build_app(self: ClientT) -> App[ClientT]: # noqa: PYI019
103
+ app: App[ClientT] = App(
104
+ connection=self._connection,
105
+ config=self._config,
106
+ auth_flow=self._auth_flow,
107
+ root_router=self._router,
108
+ )
109
+ app.dispatcher.bind_client(self)
110
+ return app
111
+
112
+ def _reset_runtime(self: ClientT) -> None: # noqa: PYI019
113
+ self._connection = self._build_connection()
114
+ self._app = self._build_app()
115
+
116
+ async def start(self: ClientT) -> None: # noqa: PYI019
117
+ """Запускает клиента и слушает события до закрытия соединения."""
118
+ while True:
119
+ try:
120
+ await self._app.start()
121
+ await self._app.dispatcher.emit_start(self)
122
+ await self._connection.wait_closed()
123
+ except asyncio.CancelledError:
124
+ await self.close()
125
+ raise
126
+ except ( # noqa: PERF203
127
+ ConnectionError,
128
+ EOFError,
129
+ OSError,
130
+ TimeoutError,
131
+ ):
132
+ await self.close()
133
+ if not self.extra_config.reconnect:
134
+ raise
135
+
136
+ logger.exception(
137
+ "client connection failed; reconnecting in %s seconds",
138
+ self.extra_config.reconnect_delay,
139
+ )
140
+ await asyncio.sleep(self.extra_config.reconnect_delay)
141
+ self._reset_runtime()
142
+ except Exception:
143
+ await self.close()
144
+ raise
145
+ else:
146
+ await self.close()
147
+ return
148
+
149
+ async def close(self) -> None:
150
+ """Закрывает соединение, фоновые задачи и файл сессии."""
151
+ await self._app.close()
152
+
153
+ async def stop(self) -> None:
154
+ """Останавливает клиента."""
155
+ await self.close()
156
+
157
+ async def __aenter__(self: ClientT) -> ClientT: # noqa: PYI019
158
+ return self
159
+
160
+ async def __aexit__(self, *args: object) -> None:
161
+ await self.close()
162
+
163
+ def on_start(self) -> StartDecorator[ClientT]:
164
+ """Регистрирует обработчик успешного запуска."""
165
+ return self._router.on_start()
166
+
167
+ def on_message(
168
+ self,
169
+ *filters: FilterCallback[Message],
170
+ ) -> HandlerDecorator[Message, ClientT]:
171
+ """Регистрирует обработчик новых сообщений."""
172
+ return self._router.on_message(*filters)
173
+
174
+ def on_message_edit(
175
+ self,
176
+ *filters: FilterCallback[Message],
177
+ ) -> HandlerDecorator[Message, ClientT]:
178
+ """Регистрирует обработчик редактирования сообщений."""
179
+ return self._router.on_message_edit(*filters)
180
+
181
+ def on_message_delete(
182
+ self,
183
+ *filters: FilterCallback[MessageDeleteEvent],
184
+ ) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
185
+ """Регистрирует обработчик удаления сообщений."""
186
+ return self._router.on_message_delete(*filters)
187
+
188
+ def on_chat_update(
189
+ self,
190
+ *filters: FilterCallback[Chat],
191
+ ) -> HandlerDecorator[Chat, ClientT]:
192
+ """Регистрирует обработчик обновления чата."""
193
+ return self._router.on_chat_update(*filters)
194
+
195
+ def on_raw(
196
+ self,
197
+ *filters: FilterCallback[InboundFrame],
198
+ ) -> HandlerDecorator[InboundFrame, ClientT]:
199
+ """Регистрирует обработчик исходных входящих frame-ов."""
200
+ return self._router.on_raw(*filters)
201
+
202
+ def include_router(self, router: Router[ClientT]) -> None:
203
+ """Подключает дочерний router к root router клиента."""
204
+ self._router.include_router(router)
pymax/client.py ADDED
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ from pymax.auth import (
4
+ AuthFlow,
5
+ ConsoleSmsCodeProvider,
6
+ PasswordProvider,
7
+ SmsAuthFlow,
8
+ SmsCodeProvider,
9
+ )
10
+ from pymax.connection import ConnectionManager
11
+ from pymax.connection.readers import TCPReader
12
+ from pymax.logging import configure_logging, get_logger
13
+ from pymax.protocol.tcp import TcpProtocol
14
+ from pymax.protocol.tcp.framing import TcpPacketFramer
15
+ from pymax.transport.tcp import TCPTransport
16
+
17
+ from .base import BaseClient
18
+ from .config import ExtraConfig
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ class Client(BaseClient["Client"]):
24
+ """TCP-клиент PyMax с SMS-авторизацией.
25
+
26
+ Используйте ``Client`` как основной long-running клиент: он открывает
27
+ соединение, проходит авторизацию, хранит сессию в ``work_dir``.
28
+ Клиент вызывает роутеры и предоставляет методы для сообщений, чатов,
29
+ пользователей и профиля.
30
+
31
+ Args:
32
+ phone: Номер телефона для первой авторизации.
33
+ session_name: Имя SQLite-файла сессии внутри ``work_dir``.
34
+ work_dir: Директория для файла сессии и служебного cache.
35
+ extra_config: Дополнительные настройки соединения, логов, reconnect
36
+ и sync.
37
+ auth_flow: Полностью пользовательский сценарий авторизации.
38
+ sms_code_provider: Провайдер SMS-кода для стандартного ``SmsAuthFlow``.
39
+ password_provider: Провайдер пароля 2FA, если аккаунт его требует.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ phone: str,
45
+ session_name: str = "session.db",
46
+ work_dir: str = ".",
47
+ extra_config: ExtraConfig | None = None,
48
+ auth_flow: AuthFlow | None = None,
49
+ sms_code_provider: SmsCodeProvider | None = None,
50
+ password_provider: PasswordProvider | None = None,
51
+ ) -> None:
52
+ self.phone = phone
53
+ self.extra_config = extra_config or ExtraConfig()
54
+ self.session_name = session_name
55
+ self.work_dir = work_dir
56
+
57
+ configure_logging(self.extra_config.log_level)
58
+ logger.debug(
59
+ "creating client phone_set=%s session=%s work_dir=%s proxy_set=%s reconnect=%s",
60
+ bool(phone),
61
+ self.session_name,
62
+ self.work_dir,
63
+ bool(self.extra_config.proxy),
64
+ self.extra_config.reconnect,
65
+ )
66
+
67
+ self._config = self._build_config(
68
+ phone=phone,
69
+ user_agent=(self.extra_config.user_agent or self.extra_config.generate_user_agent()),
70
+ )
71
+
72
+ if auth_flow is None:
73
+ auth_flow = SmsAuthFlow(
74
+ sms_code_provider or ConsoleSmsCodeProvider(),
75
+ password_provider,
76
+ )
77
+ self._init_runtime(auth_flow=auth_flow)
78
+
79
+ logger.debug(
80
+ "client created transport=tcp host=%s port=%s",
81
+ self.extra_config.host,
82
+ self.extra_config.port,
83
+ )
84
+
85
+ def _build_connection(self) -> ConnectionManager:
86
+ logger.debug(
87
+ "building tcp connection host=%s port=%s ssl=%s",
88
+ self.extra_config.host,
89
+ self.extra_config.port,
90
+ self.extra_config.use_ssl,
91
+ )
92
+ transport = TCPTransport(
93
+ port=self.extra_config.port,
94
+ host=self.extra_config.host,
95
+ use_ssl=self.extra_config.use_ssl,
96
+ proxy=self.extra_config.proxy,
97
+ )
98
+ reader = TCPReader(
99
+ transport=transport,
100
+ framer=TcpPacketFramer(),
101
+ )
102
+ return ConnectionManager(
103
+ reader=reader,
104
+ transport=transport,
105
+ protocol=TcpProtocol(),
106
+ )
pymax/client_web.py ADDED
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ from pymax.auth.providers import ConsoleQrHandler, QrHandler
4
+ from pymax.auth.qr import QrAuthFlow
5
+ from pymax.connection import ConnectionManager
6
+ from pymax.connection.readers import WSReader
7
+ from pymax.logging import configure_logging, get_logger
8
+ from pymax.protocol.ws import WsProtocol
9
+ from pymax.transport.websocket import WebSocketTransport
10
+
11
+ from .base import BaseClient
12
+ from .config import ExtraConfig
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class WebClient(BaseClient["WebClient"]):
18
+ """WebSocket-клиент PyMax с QR-авторизацией.
19
+
20
+ Используйте ``WebClient``, когда нужен web-режим Max: клиент открывает
21
+ WebSocket, показывает QR через ``qr_provider`` и после подтверждения
22
+ сохраняет сессию в ``work_dir/session_name``.
23
+
24
+ Args:
25
+ session_name: Имя SQLite-файла сессии внутри ``work_dir``.
26
+ work_dir: Директория для файла сессии.
27
+ extra_config: Настройки URL, логов, reconnect, device и sync.
28
+ auth_flow: Пользовательский QR auth-flow.
29
+ qr_provider: Обработчик, который показывает QR пользователю.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ session_name: str = "session.db",
35
+ work_dir: str = ".",
36
+ extra_config: ExtraConfig | None = None,
37
+ auth_flow: QrAuthFlow | None = None,
38
+ qr_provider: QrHandler | None = None,
39
+ ) -> None:
40
+ self.extra_config = extra_config or ExtraConfig()
41
+ self.session_name = session_name
42
+ self.work_dir = work_dir
43
+
44
+ configure_logging(self.extra_config.log_level)
45
+ logger.debug(
46
+ "creating web client session=%s work_dir=%s proxy_set=%s reconnect=%s",
47
+ self.session_name,
48
+ self.work_dir,
49
+ bool(self.extra_config.proxy),
50
+ self.extra_config.reconnect,
51
+ )
52
+
53
+ self._config = self._build_config(
54
+ phone=None,
55
+ user_agent=(
56
+ self.extra_config.user_agent or self.extra_config.generate_web_user_agent()
57
+ ),
58
+ )
59
+
60
+ if auth_flow is None:
61
+ auth_flow = QrAuthFlow(qr_provider or ConsoleQrHandler())
62
+ self._init_runtime(auth_flow=auth_flow)
63
+
64
+ logger.debug(
65
+ "web client created transport=ws url=%s",
66
+ self.extra_config.url,
67
+ )
68
+
69
+ def _build_connection(self) -> ConnectionManager:
70
+ logger.debug(
71
+ "building websocket connection url=%s",
72
+ self.extra_config.url,
73
+ )
74
+ transport = WebSocketTransport(
75
+ url=self.extra_config.url,
76
+ proxy=self.extra_config.proxy,
77
+ )
78
+ reader = WSReader(transport=transport)
79
+ return ConnectionManager(
80
+ reader=reader,
81
+ transport=transport,
82
+ protocol=WsProtocol(),
83
+ )