2026-10-03 07:03:35 +08:00
|
|
|
"""Strict account RPC allowlist. Never falls back to a device key or retries."""
|
|
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
from pigway_frame_flow.errors import ErrorCode as E
|
|
|
|
|
from pigway_frame_flow.errors import FlowError
|
|
|
|
|
from pigway_frame_flow.integrations.kokonna_official.capabilities import READ_OPERATIONS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OfficialRPC:
|
|
|
|
|
def __init__(self, token: str = ""):
|
|
|
|
|
self._token = token
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def authorized(self) -> bool:
|
|
|
|
|
return bool(self._token)
|
|
|
|
|
|
|
|
|
|
def _call(self, method: str, params: dict) -> dict:
|
|
|
|
|
if method not in READ_OPERATIONS and method != "app.login":
|
|
|
|
|
raise FlowError(
|
|
|
|
|
E.UNSUPPORTED_CAPABILITY, "Official operation is not enabled", 422
|
|
|
|
|
)
|
|
|
|
|
if method != "app.login" and not self._token:
|
|
|
|
|
raise FlowError(
|
|
|
|
|
E.CONFIG_REQUIRED, "Official account authorization required", 503
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
response = requests.post(
|
|
|
|
|
"https://robot.galaxyguide.cn/api/rpc",
|
|
|
|
|
headers={"Authorization": "Bearer " + self._token}
|
|
|
|
|
if method != "app.login"
|
|
|
|
|
else {},
|
|
|
|
|
json={
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
"method": method,
|
|
|
|
|
"params": params,
|
|
|
|
|
"id": int(time.time() * 1000),
|
|
|
|
|
},
|
|
|
|
|
timeout=15,
|
|
|
|
|
allow_redirects=False,
|
|
|
|
|
)
|
|
|
|
|
if response.status_code in (401, 403):
|
2026-10-05 00:21:17 +08:00
|
|
|
self.disconnect()
|
2026-10-03 07:03:35 +08:00
|
|
|
raise FlowError(
|
2026-10-05 00:21:17 +08:00
|
|
|
E.OFFICIAL_AUTH_REQUIRED,
|
|
|
|
|
"Official account authentication required",
|
|
|
|
|
401,
|
2026-10-03 07:03:35 +08:00
|
|
|
)
|
|
|
|
|
payload = response.json()
|
|
|
|
|
error = payload.get("error")
|
|
|
|
|
if isinstance(error, dict) and error.get("message") == "请重新登陆":
|
2026-10-05 00:21:17 +08:00
|
|
|
self.disconnect()
|
|
|
|
|
raise FlowError(
|
|
|
|
|
E.OFFICIAL_AUTH_REQUIRED, "Official account session expired", 401
|
|
|
|
|
)
|
2026-10-03 07:03:35 +08:00
|
|
|
response.raise_for_status()
|
|
|
|
|
if error or not isinstance(payload.get("result"), dict):
|
|
|
|
|
raise ValueError()
|
|
|
|
|
return payload["result"]
|
|
|
|
|
except FlowError:
|
|
|
|
|
raise
|
|
|
|
|
except (requests.RequestException, ValueError, AttributeError):
|
|
|
|
|
raise FlowError(
|
|
|
|
|
E.CLOUD_UNAVAILABLE, "Official service query failed", 502
|
|
|
|
|
) from None
|
|
|
|
|
|
|
|
|
|
def login(self, username: str, password: str) -> None:
|
|
|
|
|
if not username or not password:
|
|
|
|
|
raise FlowError(E.INVALID_REQUEST, "Official account credentials required")
|
|
|
|
|
result = self._call("app.login", {"username": username, "password": password})
|
|
|
|
|
token = result.get("token")
|
|
|
|
|
if not isinstance(token, str) or not token:
|
|
|
|
|
raise FlowError(E.UNAUTHORIZED, "Official account login failed", 401)
|
|
|
|
|
self._token = token
|
|
|
|
|
|
|
|
|
|
def disconnect(self) -> None:
|
|
|
|
|
self._token = ""
|
|
|
|
|
|
|
|
|
|
def devices(self) -> dict:
|
|
|
|
|
return self._call("app.getRobots", {})
|
|
|
|
|
|
|
|
|
|
def device(self, robot_id: int) -> dict:
|
|
|
|
|
return self._call("appFrame.getFrame", {"id": robot_id})
|
|
|
|
|
|
|
|
|
|
def widgets(self, robot_id: int) -> dict:
|
|
|
|
|
return self._call("appFrame.getFrameCovers", {"robotId": robot_id})
|