Switch MAX bridge to PyMax only
This commit is contained in:
@@ -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())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user