Switch MAX bridge to PyMax only

This commit is contained in:
sevenhill
2026-07-07 14:18:49 +03:00
parent 8efeff334a
commit af84343e19
19 changed files with 314 additions and 8088 deletions
+83 -7
View File
@@ -5,11 +5,13 @@ import base64
import mimetypes
import os
import pathlib
import shutil
import stat
import time
import traceback
import urllib.parse
from collections.abc import Awaitable, Callable
from contextlib import suppress
from datetime import UTC, datetime
from typing import Any
@@ -54,6 +56,35 @@ def normalize_phone(phone: str) -> str:
return phone.strip()
def is_expired_session_error(error: str | None) -> bool:
text = (error or "").lower()
return any(
marker in text
for marker in (
"fail_login_token",
"login.token",
"login token",
"authorize again",
"authorise again",
"please authorize",
"please authorise",
)
)
def archive_session_dir() -> str | None:
if not SESSION_DIR.exists():
return None
stamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
target = SESSION_DIR.with_name(f"{SESSION_DIR.name}.expired-{stamp}")
suffix = 1
while target.exists():
target = SESSION_DIR.with_name(f"{SESSION_DIR.name}.expired-{stamp}-{suffix}")
suffix += 1
shutil.move(str(SESSION_DIR), str(target))
return str(target)
def dump_model(obj: Any) -> dict[str, Any]:
if obj is None:
return {}
@@ -271,6 +302,13 @@ class DeferredCodeProvider:
self._future.set_result(code)
return True
def reset(self) -> None:
if self._future is not None and not self._future.done():
self._future.cancel()
self._future = None
self.waiting_for_code = False
self.requested_phone = None
class DeferredPasswordProvider:
def __init__(self) -> None:
@@ -295,6 +333,13 @@ class DeferredPasswordProvider:
self._future.set_result(password)
return True
def reset(self) -> None:
if self._future is not None and not self._future.done():
self._future.cancel()
self._future = None
self.waiting_for_password = False
self.hint = None
class PyMaxRuntime:
def __init__(self) -> None:
@@ -319,10 +364,15 @@ class PyMaxRuntime:
return "Password"
if self.code_provider.waiting_for_code:
return "Code"
if self.has_expired_session():
return "Expired"
if self.task is not None and not self.task.done():
return "Starting"
return "Idle"
def has_expired_session(self) -> bool:
return is_expired_session_error(self.last_error)
async def ensure_started(self, phone: str | None = None) -> None:
async with self.lock:
if self.task is not None and not self.task.done():
@@ -375,6 +425,8 @@ class PyMaxRuntime:
return False
async def get_client(self) -> Client:
if self.has_expired_session():
raise RuntimeError(self.last_error)
await self.ensure_started()
if not await self.wait_ready(15):
raise RuntimeError(self.last_error or f"PyMax is not ready; stage={self.login_stage()}")
@@ -382,10 +434,30 @@ class PyMaxRuntime:
return self.client
async def stop(self) -> None:
if self.client is not None:
await self.client.stop()
if self.task is not None:
self.task.cancel()
async with self.lock:
await self._stop_locked()
async def reset_session_for_login(self) -> None:
async with self.lock:
await self._stop_locked()
self.last_error = None
archive_session_dir()
async def _stop_locked(self) -> None:
client = self.client
task = self.task
self.client = None
self.task = None
self.ready.clear()
self.code_provider.reset()
self.password_provider.reset()
if client is not None:
with suppress(Exception):
await client.stop()
if task is not None and not task.done():
task.cancel()
with suppress(asyncio.CancelledError):
await task
runtime = PyMaxRuntime()
@@ -396,11 +468,12 @@ def json_response(data: Any, status: int = 200) -> web.Response:
def status_payload() -> dict[str, Any]:
stage = runtime.login_stage()
return {
"mode": "PyMax",
"isAuthorized": runtime.is_authorized,
"loginStage": runtime.login_stage(),
"status": "Ready" if runtime.is_authorized else runtime.login_stage(),
"loginStage": stage,
"status": "Ready" if runtime.is_authorized else ("SessionExpired" if stage == "Expired" else stage),
"url": None,
"title": runtime.last_title,
"lastError": runtime.last_error,
@@ -728,7 +801,8 @@ async def health(_request: web.Request) -> web.Response:
@route_errors
async def status(_request: web.Request) -> web.Response:
await runtime.ensure_started()
if not runtime.has_expired_session():
await runtime.ensure_started()
return json_response(status_payload())
@@ -736,6 +810,8 @@ async def status(_request: web.Request) -> web.Response:
async def login_start(request: web.Request) -> web.Response:
data = await read_json(request)
phone = str(data.get("phoneNumber") or PHONE_NUMBER).strip()
if not runtime.is_authorized and runtime.login_stage() not in {"Code", "Password"}:
await runtime.reset_session_for_login()
await runtime.ensure_started(phone)
return json_response(status_payload())