Files
abot/wechat_ipad/providers/server_864/login.py

147 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import qrcode
from urllib.parse import parse_qs, urlparse
from wechat_ipad.providers.server_864.base import Server864APIClientBase
class LoginMixin(Server864APIClientBase):
"""864 登录相关接口。"""
@staticmethod
def _normalize_login_way(login_way: str) -> str:
"""标准化 864 `GetLoginQrCodeNewX` 的 way 参数。"""
normalized_way = str(login_way or "").strip().lower()
return normalized_way if normalized_way in {"mac", "win", "harmony", "car", "watch"} else "mac"
def _extract_qr_response(self, data) -> tuple[str, str]:
"""从 864 的二维码返回结构中提取 uuid 与二维码地址。"""
qr_code_url = (
self._pick_first(data, "QrCodeUrl", "QRCodeUrl", "qrCodeUrl")
or self._pick_first(data, "QrUrl", "QRUrl", "qrUrl")
or self._pick_first(self._pick_first(data, "Qrcode", "QrCode", "qrcode") or {}, "Src", "src")
or ""
)
uuid = self._pick_first(data, "UUID", "Uuid", "uuid") or ""
if not uuid and qr_code_url:
# 864 的真实返回经常只给“二维码图片地址”,并不会直接带出 uuid
# 1. 图床链接 query 中的 `data=http://weixin.qq.com/x/<uuid>` 才是真正扫码值;
# 2. Dashboard 当前以 uuid 作为刷新与历史记录主键,缺失后前端体验会明显变差;
# 3. 因此这里统一做一次反解,让 New / NewX 两种接口都复用同一份展示逻辑。
parsed_qs = parse_qs(urlparse(str(qr_code_url)).query)
scan_data = str((parsed_qs.get("data") or [""])[0] or "")
if "/x/" in scan_data:
uuid = scan_data.rsplit("/x/", 1)[-1].strip()
return str(uuid), str(qr_code_url)
async def get_qr_code(
self,
device_name: str = "",
device_id: str = "",
proxy=None,
print_qr: bool = False,
login_qr_api: str = "new_x",
login_way: str = "mac",
) -> tuple[str, str]:
"""获取 864 登录二维码。
说明:
1. 864 不依赖 855 的 `device_name/device_id` 入参,但保留参数签名以兼容上层调用;
2. `proxy` 当前仅保留兼容占位,后续如需补实际代理登录,可直接映射到 swagger 的 Proxy 字段;
3. 返回值继续保持 `(uuid, url)`,方便 Dashboard 与运行时共用同一套二维码展示逻辑。
"""
del device_name, device_id
proxy_value = ""
if proxy is not None:
proxy_value = getattr(proxy, "proxy", "") or ""
normalized_login_qr_api = str(login_qr_api or "new_x").strip().lower()
normalized_login_way = self._normalize_login_way(login_way)
if normalized_login_qr_api in {"new_x", "x", "newx"}:
try:
# NewX 是当前 864 联调里更完整的一条登录链路:
# 1. 它支持 `Way` 指定登录端形态,兼容更多 server 变体;
# 2. 用户实测也验证该接口可正常返回二维码;
# 3. 若某些旧版 864 仍未提供 NewX则自动回退到旧接口避免新配置直接打挂启动。
data = await self._request_data(
"post",
"/login/GetLoginQrCodeNewX",
json_body={"Proxy": proxy_value, "Check": False, "Way": normalized_login_way},
timeout=30,
)
uuid, qr_url = self._extract_qr_response(data)
except Exception:
data = await self._request_data(
"post",
"/login/GetLoginQrCodeNew",
json_body={"Proxy": proxy_value, "Check": False},
timeout=30,
)
uuid, qr_url = self._extract_qr_response(data)
else:
data = await self._request_data(
"post",
"/login/GetLoginQrCodeNew",
json_body={"Proxy": proxy_value, "Check": False},
timeout=30,
)
uuid, qr_url = self._extract_qr_response(data)
if print_qr and uuid:
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(f"http://weixin.qq.com/x/{uuid}")
qr.make(fit=True)
qr.print_ascii()
return str(uuid), str(qr_url)
async def check_login_status(self) -> tuple[bool, dict]:
"""检查当前二维码登录状态。"""
data = await self._request_data("get", "/login/CheckLoginStatus", timeout=20)
normalized = dict(data or {})
# 864 扫码后可能不会直接变成 online而是先返回一个安全验证链接
# 1. 该字段在 swagger / 实机联调里都存在,但大小写并不稳定;
# 2. 上层 runtime 只关心统一字段名,因此这里先做一层归一化;
# 3. 这样 Dashboard 只需要消费 `verification_url`,不用感知各 server 的原始差异。
verification_url = self._pick_first(
normalized,
"VerificationUrl",
"VerificationURL",
"verificationUrl",
"verification_url",
)
if verification_url:
normalized["verification_url"] = str(verification_url).strip()
state = int(normalized.get("state", 0) or 0)
login_state = str(normalized.get("loginState", "") or "").strip().lower()
return state == 2 or login_state == "online", normalized
async def get_init_status(self) -> bool:
"""检查 server 侧初始化是否完成。"""
data = await self._request_data("get", "/login/GetInItStatus", timeout=15)
return bool(data)
async def awaken_login(self, wxid: str = "") -> dict:
"""触发 864 的唤醒登录。"""
del wxid
return await self._request_data("post", "/login/WakeUpLogin", timeout=30)
async def get_login_status(self, auto_login: bool = True) -> dict:
"""获取 864 在线状态。"""
return await self._request_data(
"get",
"/login/GetLoginStatus",
params={"autoLogin": str(bool(auto_login)).lower()},
timeout=20,
)
async def log_out(self) -> bool:
"""退出当前 864 登录态。"""
await self._request_data("get", "/login/LogOutRequest", timeout=15)
return True