maxapi-python 2.4.0__py3-none-any.whl → 2.5.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 (101) hide show
  1. {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/METADATA +22 -12
  2. maxapi_python-2.5.0.dist-info/RECORD +195 -0
  3. {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +8 -2
  5. pymax/_data/apk_fingerprints.json +374 -17
  6. pymax/_data/rootca_ssl_rsa2022.crt +33 -0
  7. pymax/api/auth/service.py +15 -2
  8. pymax/api/auth/types.py +3 -0
  9. pymax/api/bots/payloads.py +8 -0
  10. pymax/api/bots/service.py +13 -1
  11. pymax/api/chats/enums.py +1 -0
  12. pymax/api/chats/payloads.py +21 -1
  13. pymax/api/chats/service.py +41 -0
  14. pymax/api/facade.py +2 -0
  15. pymax/api/messages/__init__.py +1 -1
  16. pymax/api/messages/enums.py +1 -0
  17. pymax/api/messages/payloads.py +56 -4
  18. pymax/api/messages/service.py +405 -27
  19. pymax/api/response.py +1 -1
  20. pymax/api/stories/__init__.py +2 -0
  21. pymax/api/stories/enums.py +19 -0
  22. pymax/api/stories/payloads.py +64 -0
  23. pymax/api/stories/service.py +198 -0
  24. pymax/api/uploads/payloads.py +42 -1
  25. pymax/api/uploads/service.py +171 -69
  26. pymax/app.py +39 -8
  27. pymax/auth/__init__.py +2 -0
  28. pymax/auth/exceptions.py +9 -0
  29. pymax/auth/providers.py +6 -2
  30. pymax/auth/qr.py +11 -1
  31. pymax/auth/sms.py +11 -1
  32. pymax/base.py +146 -27
  33. pymax/client.py +39 -11
  34. pymax/client_web.py +15 -12
  35. pymax/config.py +101 -53
  36. pymax/di/__init__.py +2 -0
  37. pymax/di/context.py +18 -0
  38. pymax/di/resolver.py +38 -0
  39. pymax/dispatch/__init__.py +7 -0
  40. pymax/dispatch/dispatcher.py +143 -40
  41. pymax/dispatch/enums.py +1 -0
  42. pymax/dispatch/mapping.py +23 -3
  43. pymax/dispatch/middleware.py +32 -0
  44. pymax/dispatch/resolvers.py +4 -0
  45. pymax/dispatch/router.py +86 -34
  46. pymax/files/base.py +58 -1
  47. pymax/files/video.py +32 -40
  48. pymax/files/voice.py +18 -4
  49. pymax/filters/__init__.py +15 -0
  50. pymax/filters/base.py +218 -0
  51. pymax/filters/containers/__init__.py +2 -0
  52. pymax/filters/containers/chat.py +240 -0
  53. pymax/filters/containers/message.py +147 -0
  54. pymax/filters/filters.py +211 -0
  55. pymax/filters/magic.py +13 -0
  56. pymax/filters/presets.py +88 -0
  57. pymax/filters/refs.py +56 -0
  58. pymax/fingerprint/fingerprint.py +5 -22
  59. pymax/fingerprint/models.py +1 -0
  60. pymax/formatting/markdown.py +55 -1
  61. pymax/fsm/__init__.py +3 -0
  62. pymax/fsm/context.py +30 -0
  63. pymax/fsm/protocol.py +14 -0
  64. pymax/fsm/state.py +70 -0
  65. pymax/fsm/storage.py +151 -0
  66. pymax/infra/base.py +2 -0
  67. pymax/infra/bots.py +20 -0
  68. pymax/infra/chat.py +37 -5
  69. pymax/infra/message.py +204 -9
  70. pymax/infra/protocol.py +5 -1
  71. pymax/infra/stories.py +205 -0
  72. pymax/integrations/__init__.py +0 -0
  73. pymax/integrations/dishka.py +109 -0
  74. pymax/protocol/enums.py +2 -0
  75. pymax/protocol/tcp/payload.py +0 -2
  76. pymax/session/__init__.py +1 -1
  77. pymax/session/models.py +30 -0
  78. pymax/session/protocol.py +33 -7
  79. pymax/session/store.py +73 -5
  80. pymax/transport/tcp.py +11 -3
  81. pymax/types/domain/__init__.py +23 -1
  82. pymax/types/domain/attachments/__init__.py +1 -1
  83. pymax/types/domain/attachments/call.py +7 -1
  84. pymax/types/domain/attachments/contact.py +3 -3
  85. pymax/types/domain/attachments/control.py +3 -0
  86. pymax/types/domain/attachments/enums.py +16 -0
  87. pymax/types/domain/attachments/keyboards/__init__.py +1 -1
  88. pymax/types/domain/attachments/keyboards/enums.py +12 -0
  89. pymax/types/domain/attachments/keyboards/inline.py +74 -3
  90. pymax/types/domain/attachments/video.py +3 -3
  91. pymax/types/domain/callback.py +19 -0
  92. pymax/types/domain/comment.py +24 -0
  93. pymax/types/domain/enums.py +7 -0
  94. pymax/types/domain/message.py +158 -9
  95. pymax/types/domain/story.py +126 -0
  96. pymax/types/events/reaction.py +28 -4
  97. pymax/versions/__init__.py +2 -0
  98. pymax/versions/catalog.py +104 -0
  99. pymax/versions/exceptions.py +10 -0
  100. maxapi_python-2.4.0.dist-info/RECORD +0 -161
  101. {maxapi_python-2.4.0.dist-info → maxapi_python-2.5.0.dist-info}/licenses/LICENSE +0 -0
pymax/client.py CHANGED
@@ -7,12 +7,15 @@ from pymax.auth import (
7
7
  SmsAuthFlow,
8
8
  SmsCodeProvider,
9
9
  )
10
+ from pymax.config import ClientConfig
10
11
  from pymax.connection import ConnectionManager
11
12
  from pymax.connection.readers import TCPReader
13
+ from pymax.dispatch import Router
12
14
  from pymax.logging import configure_logging, get_logger
13
15
  from pymax.protocol.tcp import TcpProtocol
14
16
  from pymax.protocol.tcp.framing import TcpPacketFramer
15
17
  from pymax.transport.tcp import TCPTransport
18
+ from pymax.versions.catalog import VersionCatalog
16
19
 
17
20
  from .base import BaseClient
18
21
  from .config import ExtraConfig
@@ -37,6 +40,15 @@ class Client(BaseClient["Client"]):
37
40
  auth_flow: Полностью пользовательский сценарий авторизации.
38
41
  sms_code_provider: Провайдер SMS-кода для стандартного ``SmsAuthFlow``.
39
42
  password_provider: Провайдер пароля 2FA, если аккаунт его требует.
43
+ app_version: Версия Android-клиента для user-agent и fingerprint.
44
+ По умолчанию используется ``VersionCatalog.recommended()``.
45
+ catalog: Каталог fingerprints. Если не передан, используется
46
+ встроенный каталог. Каталог и версия разрешаются при
47
+ :meth:`connect` или :meth:`start`.
48
+
49
+ Raises:
50
+ VersionNotFoundError: При запуске, если ``app_version`` отсутствует в
51
+ итоговом каталоге.
40
52
  """
41
53
 
42
54
  def __init__( # noqa: PLR0913
@@ -48,12 +60,15 @@ class Client(BaseClient["Client"]):
48
60
  auth_flow: AuthFlow | None = None,
49
61
  sms_code_provider: SmsCodeProvider | None = None,
50
62
  password_provider: PasswordProvider | None = None,
63
+ app_version: str = VersionCatalog.RECOMMENDED_APP_VERSION,
64
+ catalog: VersionCatalog | None = None,
51
65
  ) -> None:
52
-
53
- self.phone = phone
54
- self.extra_config = extra_config or ExtraConfig()
55
- self.session_name = session_name
56
- self.work_dir = work_dir
66
+ self.phone: str = phone
67
+ self.extra_config: ExtraConfig = extra_config or ExtraConfig()
68
+ self.session_name: str = session_name
69
+ self.work_dir: str = work_dir
70
+ self.app_version: str = app_version
71
+ self.catalog: VersionCatalog = catalog or VersionCatalog()
57
72
 
58
73
  configure_logging(self.extra_config.log_level)
59
74
  logger.debug(
@@ -65,17 +80,13 @@ class Client(BaseClient["Client"]):
65
80
  self.extra_config.reconnect,
66
81
  )
67
82
 
68
- self._config = self._build_config(
69
- phone=phone,
70
- user_agent=(self.extra_config.user_agent or self.extra_config.generate_user_agent()),
71
- )
72
-
73
83
  if auth_flow is None:
74
84
  auth_flow = SmsAuthFlow(
75
85
  sms_code_provider or ConsoleSmsCodeProvider(),
76
86
  password_provider,
77
87
  )
78
- self._init_runtime(auth_flow=auth_flow)
88
+ self._auth_flow = auth_flow
89
+ self._router = Router()
79
90
 
80
91
  logger.debug(
81
92
  "client created transport=tcp host=%s port=%s",
@@ -83,6 +94,23 @@ class Client(BaseClient["Client"]):
83
94
  self.extra_config.port,
84
95
  )
85
96
 
97
+ async def _prepare_config(self) -> ClientConfig:
98
+ catalog = await self.catalog.load()
99
+ fingerprint = catalog.resolve(self.app_version)
100
+
101
+ user_agent = self.extra_config.user_agent or self.extra_config.generate_user_agent(
102
+ self.app_version, fingerprint.build_number
103
+ )
104
+
105
+ config = self._build_config(
106
+ phone=self.phone,
107
+ user_agent=user_agent,
108
+ version=self.app_version,
109
+ fingerprint=fingerprint,
110
+ )
111
+ config.restore_user_agent_from_session = self.extra_config.user_agent is None
112
+ return config
113
+
86
114
  def _build_connection(self) -> ConnectionManager:
87
115
  logger.debug(
88
116
  "building tcp connection host=%s port=%s ssl=%s",
pymax/client_web.py CHANGED
@@ -5,12 +5,13 @@ from pymax.auth.providers import ConsoleQrHandler, QrHandler
5
5
  from pymax.auth.qr import QrAuthFlow
6
6
  from pymax.connection import ConnectionManager
7
7
  from pymax.connection.readers import WSReader
8
+ from pymax.dispatch import Router
8
9
  from pymax.logging import configure_logging, get_logger
9
10
  from pymax.protocol.tcp import TcpProtocol
10
11
  from pymax.transport.websocket import WebSocketTransport
11
12
 
12
13
  from .base import BaseClient
13
- from .config import ExtraConfig
14
+ from .config import ClientConfig, ExtraConfig
14
15
 
15
16
  logger = get_logger(__name__)
16
17
 
@@ -38,9 +39,9 @@ class WebClient(BaseClient["WebClient"]):
38
39
  auth_flow: AuthFlow | None = None,
39
40
  qr_provider: QrHandler | None = None,
40
41
  ) -> None:
41
- self.extra_config = extra_config or ExtraConfig()
42
- self.session_name = session_name
43
- self.work_dir = work_dir
42
+ self.extra_config: ExtraConfig = extra_config or ExtraConfig()
43
+ self.session_name: str = session_name
44
+ self.work_dir: str = work_dir
44
45
 
45
46
  configure_logging(self.extra_config.log_level)
46
47
  logger.debug(
@@ -51,22 +52,24 @@ class WebClient(BaseClient["WebClient"]):
51
52
  self.extra_config.reconnect,
52
53
  )
53
54
 
54
- self._config = self._build_config(
55
- phone=None,
56
- user_agent=(
57
- self.extra_config.user_agent or self.extra_config.generate_web_user_agent()
58
- ),
59
- )
60
-
61
55
  if auth_flow is None:
62
56
  auth_flow = QrAuthFlow(qr_provider or ConsoleQrHandler())
63
- self._init_runtime(auth_flow=auth_flow)
57
+ self._auth_flow = auth_flow
58
+ self._router = Router()
64
59
 
65
60
  logger.debug(
66
61
  "web client created transport=ws url=%s",
67
62
  self.extra_config.url,
68
63
  )
69
64
 
65
+ async def _prepare_config(self) -> ClientConfig:
66
+ user_agent = self.extra_config.user_agent or self.extra_config.generate_web_user_agent()
67
+ config = self._build_config(
68
+ phone=None, user_agent=user_agent, version=None, fingerprint=None
69
+ )
70
+ config.restore_user_agent_from_session = self.extra_config.user_agent is None
71
+ return config
72
+
70
73
  def _build_connection(self) -> ConnectionManager:
71
74
  logger.debug(
72
75
  "building websocket connection url=%s",
pymax/config.py CHANGED
@@ -1,5 +1,5 @@
1
- from random import choice, randint, random
2
- from uuid import uuid4
1
+ import secrets
2
+ from random import choice, randint
3
3
 
4
4
  from pydantic import BaseModel, ConfigDict, Field
5
5
 
@@ -8,8 +8,10 @@ from pymax.api.session.payloads import (
8
8
  DEFAULT_WEB_HEADER_USER_AGENT,
9
9
  MobileUserAgentPayload,
10
10
  )
11
+ from pymax.fingerprint.models import ApkBuildFingerprint
11
12
  from pymax.session import StoreProtocol
12
13
  from pymax.types.domain.sync import SyncOverrides
14
+ from pymax.versions.catalog import VersionCatalog
13
15
 
14
16
  MIN_PREFERRED_BUILD = 6712
15
17
  APP_VERSIONS: tuple[tuple[str, int], ...] = (
@@ -47,50 +49,56 @@ APP_VERSIONS: tuple[tuple[str, int], ...] = (
47
49
  ("26.12.2", 6681),
48
50
  ("26.12.1", 6679),
49
51
  ("26.12.0", 6676),
50
- ("26.11.3", 6670),
51
- ("26.11.2", 6669),
52
- ("26.11.1", 6665),
53
- ("26.10.1", 6653),
54
- ("26.10.0", 6648),
55
- ("26.9.1", 6643),
52
+ # ("26.11.3", 6670), # Not supported TODO: delete maybe
53
+ # ("26.11.2", 6669),
54
+ # ("26.11.1", 6665),
55
+ # ("26.10.1", 6653),
56
+ # ("26.10.0", 6648),
57
+ # ("26.9.1", 6643),
56
58
  )
57
59
  ANDROID_DEVICES: tuple[tuple[str, str, str, str], ...] = (
58
- ("Samsung SM-A525F", "Android 13", "405dpi 405dpi 1080x2400", "arm64-v8a"),
59
- ("Samsung SM-A536B", "Android 14", "405dpi 405dpi 1080x2400", "arm64-v8a"),
60
- ("Samsung SM-A546E", "Android 14", "405dpi 405dpi 1080x2340", "arm64-v8a"),
61
- ("Samsung SM-G991B", "Android 14", "421dpi 421dpi 1080x2400", "arm64-v8a"),
62
- ("Samsung SM-G998B", "Android 13", "515dpi 515dpi 1440x3200", "arm64-v8a"),
63
- ("Samsung SM-S901B", "Android 14", "425dpi 425dpi 1080x2340", "arm64-v8a"),
64
- ("Samsung SM-S911B", "Android 14", "425dpi 425dpi 1080x2340", "arm64-v8a"),
65
- ("Xiaomi 2109119DG", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
66
- ("Xiaomi 2201117TG", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
67
- ("Xiaomi 2201123G", "Android 14", "526dpi 526dpi 1440x3200", "arm64-v8a"),
68
- ("Xiaomi 2210132G", "Android 14", "446dpi 446dpi 1220x2712", "arm64-v8a"),
69
60
  (
70
- "Xiaomi 23049PCD8G",
61
+ "Google Pixel 6a",
62
+ "Android 13",
63
+ "420dpi 420dpi 1080x2400",
64
+ "arm64-v8a",
65
+ ),
66
+ (
67
+ "Google Pixel 7",
68
+ "Android 13",
69
+ "420dpi 420dpi 1080x2400",
70
+ "arm64-v8a",
71
+ ),
72
+ (
73
+ "Google Pixel 7 Pro",
74
+ "Android 13",
75
+ "560dpi 560dpi 1440x3120",
76
+ "arm64-v8a",
77
+ ),
78
+ (
79
+ "Google Pixel 8",
80
+ "Android 14",
81
+ "420dpi 420dpi 1080x2400",
82
+ "arm64-v8a",
83
+ ),
84
+ (
85
+ "samsung SM-G991B",
71
86
  "Android 14",
72
- "446dpi 446dpi 1220x2712",
87
+ "xxhdpi 480dpi 1080x2400",
88
+ "arm64-v8a",
89
+ ),
90
+ (
91
+ "samsung SM-S911B",
92
+ "Android 15",
93
+ "xxhdpi 480dpi 1080x2340",
94
+ "arm64-v8a",
95
+ ),
96
+ (
97
+ "Xiaomi 2201117TG",
98
+ "Android 13",
99
+ "440dpi 440dpi 1080x2400",
73
100
  "arm64-v8a",
74
101
  ),
75
- ("Redmi 2201116TG", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
76
- ("Redmi 22101316G", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
77
- ("Redmi 23021RAA2Y", "Android 14", "395dpi 395dpi 1080x2400", "arm64-v8a"),
78
- ("POCO 22011211G", "Android 13", "395dpi 395dpi 1080x2400", "arm64-v8a"),
79
- ("POCO 23049PCD8G", "Android 14", "446dpi 446dpi 1220x2712", "arm64-v8a"),
80
- ("Pixel 6", "Android 14", "411dpi 411dpi 1080x2400", "arm64-v8a"),
81
- ("Pixel 6a", "Android 14", "429dpi 429dpi 1080x2400", "arm64-v8a"),
82
- ("Pixel 7", "Android 14", "416dpi 416dpi 1080x2400", "arm64-v8a"),
83
- ("Pixel 7 Pro", "Android 14", "512dpi 512dpi 1440x3120", "arm64-v8a"),
84
- ("Pixel 8", "Android 14", "428dpi 428dpi 1080x2400", "arm64-v8a"),
85
- ("OnePlus NE2213", "Android 14", "525dpi 525dpi 1440x3216", "arm64-v8a"),
86
- ("OnePlus CPH2449", "Android 14", "451dpi 451dpi 1240x2772", "arm64-v8a"),
87
- ("realme RMX3085", "Android 13", "409dpi 409dpi 1080x2400", "arm64-v8a"),
88
- ("realme RMX3370", "Android 13", "409dpi 409dpi 1080x2400", "arm64-v8a"),
89
- ("realme RMX3630", "Android 13", "400dpi 400dpi 1080x2412", "arm64-v8a"),
90
- ("HUAWEI ELS-NX9", "Android 12", "441dpi 441dpi 1080x2340", "arm64-v8a"),
91
- ("HUAWEI VOG-L29", "Android 12", "398dpi 398dpi 1080x2340", "arm64-v8a"),
92
- ("HONOR RMO-NX1", "Android 13", "391dpi 391dpi 1080x2388", "arm64-v8a"),
93
- ("HONOR REA-NX9", "Android 13", "435dpi 435dpi 1200x2664", "arm64-v8a"),
94
102
  )
95
103
  LOCALE_TIMEZONES: tuple[tuple[str, str], ...] = (
96
104
  ("ru", "Europe/Moscow"),
@@ -104,17 +112,21 @@ LOCALE_TIMEZONES: tuple[tuple[str, str], ...] = (
104
112
  ("ru", "Asia/Yakutsk"),
105
113
  ("ru", "Asia/Vladivostok"),
106
114
  )
107
- WEB_APP_VERSION = "26.7.15"
115
+ WEB_APP_VERSION = "26.8.4"
108
116
  WEB_SCREEN = "1080x1920 1.0x"
109
117
 
110
118
  PREFERRED_VERSION = [version for version in APP_VERSIONS if version[1] >= MIN_PREFERRED_BUILD]
111
119
  LEGACY_VERSIONS = [version for version in APP_VERSIONS if version[1] < MIN_PREFERRED_BUILD]
112
120
 
113
121
 
122
+ def generate_device_id() -> str:
123
+ return secrets.token_hex(8)
124
+
125
+
114
126
  class DeviceConfig(BaseModel):
115
127
  mt_instance_id: str
116
128
  user_agent: MobileUserAgentPayload
117
- device_id: str = Field(default_factory=lambda: str(uuid4()))
129
+ device_id: str = Field(default_factory=generate_device_id)
118
130
  client_session_id: int = Field(default_factory=lambda: randint(1, 70))
119
131
 
120
132
 
@@ -136,6 +148,9 @@ class RegistrationConfig(BaseModel):
136
148
  class ClientConfig(BaseModel):
137
149
  model_config = ConfigDict(arbitrary_types_allowed=True)
138
150
 
151
+ app_version: str | None = VersionCatalog.RECOMMENDED_APP_VERSION
152
+ fingering: ApkBuildFingerprint | None
153
+
139
154
  phone: str | None = None
140
155
  work_dir: str = "."
141
156
  session_name: str = "session.db"
@@ -143,17 +158,21 @@ class ClientConfig(BaseModel):
143
158
  token: str | None = None
144
159
  proxy: str | None = None
145
160
  registration_config: RegistrationConfig | None = None
146
-
147
- host: str = "api.oneme.ru"
161
+ upload_timeout: int = 900
162
+ relogin: bool = True
163
+ host: str = "api2.oneme.ru"
148
164
  port: int = 443
149
165
  use_ssl: bool = True
166
+ persist_session: bool = True
150
167
 
168
+ password_max_attempts: int | None = None
151
169
  protocol_version: int = 10
152
170
  request_timeout: float = 30.0
153
171
  log_level: str = "INFO"
154
172
  telemetry: bool = False
155
173
 
156
174
  interactive: bool = True
175
+ restore_user_agent_from_session: bool = True
157
176
 
158
177
  store: StoreProtocol | None = None
159
178
 
@@ -181,13 +200,35 @@ class ExtraConfig(BaseModel):
181
200
  proxy: Proxy URL для TCP- или WebSocket-транспорта.
182
201
  reconnect: Переподключаться после сетевых ошибок.
183
202
  reconnect_delay: Пауза перед reconnect.
184
- device_id: Явный device ID. Если не передан, генерируется UUID.
203
+ upload_timeout: Общий timeout HTTP-загрузки файлов, видео, голосовых и
204
+ видеосообщений в секундах. По умолчанию ``900``; не применяется к
205
+ фотографиям и 60-секундному ожиданию обработки вложения. ``0`` и
206
+ отрицательные значения отключают общий timeout, но не отдельный
207
+ 60-секундный socket/read timeout.
208
+ relogin: Автоматически сбрасывать сессию и повторять авторизацию, если
209
+ Max отозвал login token. По умолчанию ``True``.
210
+ password_max_attempts: Максимальное число попыток ввода пароля 2FA.
211
+ ``None`` означает отсутствие лимита; ``0`` и отрицательные значения
212
+ сразу завершают авторизацию с ``PasswordAttemptsExceededError``.
213
+ persist_session: Сохранять token, device/user-agent и sync-state между
214
+ runtime в SQLite или переданном ``store``. По умолчанию ``True``.
215
+ При ``False`` используется отдельный ``InMemoryStore`` и значение
216
+ ``store`` игнорируется; после пересоздания runtime, в том числе при
217
+ reconnect, сохраненная сессия недоступна.
218
+ device_id: Явный device ID. Если не передан, генерируется строка из
219
+ 16 шестнадцатеричных символов.
185
220
  device_type: Тип устройства для mobile user-agent.
186
- user_agent: Полностью заданный user-agent payload.
187
- mt_instance_id: Instance ID устройства.
221
+ user_agent: Полностью заданный user-agent payload. Явное значение имеет
222
+ приоритет над user-agent из сохраненной сессии. Автоматически
223
+ сгенерированный user-agent восстанавливает характеристики устройства
224
+ из сессии, сохраняя версию приложения и build number текущего запуска.
225
+ mt_instance_id: Instance ID устройства. По умолчанию генерируется
226
+ строка из 16 шестнадцатеричных символов.
188
227
  request_timeout: Timeout API-запросов в секундах.
189
228
  log_level: Уровень логов ``pymax``.
190
229
  telemetry: Отправлять telemetry-события Max.
230
+ store: Пользовательское хранилище сессии. Используется только при
231
+ ``persist_session=True``.
191
232
  sync: Переопределения sync-маркеров для login.
192
233
 
193
234
  Example:
@@ -210,18 +251,22 @@ class ExtraConfig(BaseModel):
210
251
  token: str | None = None
211
252
  registration_config: RegistrationConfig | None = None
212
253
 
213
- host: str = "api.oneme.ru"
254
+ host: str = "api2.oneme.ru"
214
255
  port: int = 443
215
256
  url: str = "wss://api.oneme.ru/websocket"
216
257
  use_ssl: bool = True
217
258
  proxy: str | None = None
218
259
  reconnect: bool = True
219
260
  reconnect_delay: float = 1.0
261
+ upload_timeout: int = 900
262
+ relogin: bool = True
263
+ password_max_attempts: int | None = None
264
+ persist_session: bool = True
220
265
 
221
266
  device_id: str | None = None
222
267
  device_type: DeviceType = DeviceType.ANDROID
223
268
  user_agent: MobileUserAgentPayload | None = None
224
- mt_instance_id: str = Field(default_factory=lambda: str(uuid4()))
269
+ mt_instance_id: str = Field(default_factory=generate_device_id)
225
270
 
226
271
  request_timeout: float = 30.0
227
272
  log_level: str = "INFO"
@@ -231,14 +276,17 @@ class ExtraConfig(BaseModel):
231
276
 
232
277
  sync: SyncOverrides = Field(default_factory=SyncOverrides)
233
278
 
234
- def generate_user_agent(self) -> MobileUserAgentPayload:
279
+ def generate_user_agent(self, app_version: str, build_number: int) -> MobileUserAgentPayload:
235
280
  """Создает mobile user-agent payload для TCP-клиента.
236
281
 
282
+ Args:
283
+ app_version: Версия Android-клиента Max.
284
+ build_number: Build number той же версии из ``VersionCatalog``.
285
+
237
286
  Returns:
238
- Случайная, но правдоподобная конфигурация Android-клиента Max.
287
+ Случайная конфигурация Android-устройства с переданными версией и
288
+ build number.
239
289
  """
240
- versions = PREFERRED_VERSION if random() < 0.9 else LEGACY_VERSIONS
241
- app_version, build_number = choice(versions)
242
290
  device_name, os_version, screen, arch = choice(ANDROID_DEVICES)
243
291
  locale, timezone = choice(LOCALE_TIMEZONES)
244
292
 
pymax/di/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .context import DiContext
2
+ from .resolver import DiResolver
pymax/di/context.py ADDED
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Generic, TypeVar
5
+
6
+ if TYPE_CHECKING:
7
+ from pymax.app import App
8
+ from pymax.base import BaseClient
9
+ from pymax.dispatch.mapping import DispatchEvent
10
+
11
+ ClientT = TypeVar("ClientT", bound="BaseClient")
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class DiContext(Generic[ClientT]):
16
+ app: App[ClientT]
17
+ event: DispatchEvent
18
+ client: ClientT
pymax/di/resolver.py ADDED
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from logging import getLogger
5
+ from typing import Any, TypeAlias
6
+
7
+ from pymax.fsm import FSMContext
8
+ from pymax.types import Message
9
+
10
+ from .context import DiContext
11
+
12
+ logger = getLogger()
13
+
14
+
15
+ async def resolve_fsm(ctx: DiContext) -> FSMContext | None:
16
+ if not isinstance(ctx.event, Message):
17
+ raise ValueError("Fsm is not currently working for non-message handlers.")
18
+
19
+ if not ctx.event.chat_id or not ctx.event.sender:
20
+ logger.warning("Not enough data in message event. Skipping fsm")
21
+ return None
22
+
23
+ return ctx.app.fsm_storage.get_context(ctx.client, ctx.event)
24
+
25
+
26
+ Resolver: TypeAlias = Callable[[DiContext], Awaitable[Any]]
27
+
28
+ DI_MAP: dict[type, Resolver] = {FSMContext: resolve_fsm}
29
+
30
+
31
+ class DiResolver:
32
+ async def resolve(self, annotation: Any, ctx: DiContext) -> None | Any:
33
+ resolver = DI_MAP.get(annotation)
34
+
35
+ if resolver is None:
36
+ return None
37
+
38
+ return await resolver(ctx)
@@ -1,11 +1,14 @@
1
1
  from .dispatcher import Dispatcher
2
2
  from .enums import EventType
3
+ from .mapping import DispatchEvent
4
+ from .middleware import Middleware, MiddlewareNext
3
5
  from .router import (
4
6
  ClientRouter,
5
7
  DisconnectCallback,
6
8
  DisconnectDecorator,
7
9
  ErrorContext,
8
10
  ErrorScope,
11
+ HandlerCallback,
9
12
  Router,
10
13
  )
11
14
 
@@ -13,9 +16,13 @@ __all__ = (
13
16
  "ClientRouter",
14
17
  "DisconnectCallback",
15
18
  "DisconnectDecorator",
19
+ "DispatchEvent",
16
20
  "Dispatcher",
17
21
  "ErrorContext",
18
22
  "ErrorScope",
19
23
  "EventType",
24
+ "HandlerCallback",
25
+ "Middleware",
26
+ "MiddlewareNext",
20
27
  "Router",
21
28
  )