执行 Ruff 安全自动修复

This commit is contained in:
yml2213
2026-08-31 10:28:14 +08:00
parent 2a1d27f953
commit b58c6b4357
145 changed files with 940 additions and 993 deletions
+10 -10
View File
@@ -1,11 +1,11 @@
"""斗鱼登录模块"""
from .login import DouyuLogin, CredentialError
from .login_api import LoginAPIStrategy
from .login_api_wgapi import WgapiLoginAPI
from .login_api_iframe import IframeLoginAPI
from .email_verifier import EmailVerifier
from .activity_client import DouyuActivityClient, DouyuActivityError
from .email_verifier import EmailVerifier
from .login import CredentialError, DouyuLogin
from .login_api import LoginAPIStrategy
from .login_api_iframe import IframeLoginAPI
from .login_api_wgapi import WgapiLoginAPI
from .recharge_api import (
FishFinRechargeClient,
FishFinRechargeConfig,
@@ -14,16 +14,16 @@ from .recharge_api import (
)
__all__ = [
"DouyuLogin",
"CredentialError",
"LoginAPIStrategy",
"WgapiLoginAPI",
"IframeLoginAPI",
"EmailVerifier",
"DouyuActivityClient",
"DouyuActivityError",
"DouyuLogin",
"EmailVerifier",
"FishFinRechargeClient",
"FishFinRechargeConfig",
"FishFinRechargeConfigError",
"FishFinRechargeError",
"IframeLoginAPI",
"LoginAPIStrategy",
"WgapiLoginAPI",
]
+1 -3
View File
@@ -18,7 +18,6 @@ from .cookie_utils import (
normalize_douyu_cookie,
)
PC_UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
@@ -1446,8 +1445,7 @@ class DouyuActivityClient:
if not match:
raise DouyuActivityError(f"响应中找不到 var {varname}=: {text[:200]}")
chunk = text[match.end() :].strip()
if chunk.endswith(";"):
chunk = chunk[:-1]
chunk = chunk.removesuffix(";")
try:
return json.loads(chunk)
except json.JSONDecodeError as exc:
+2 -4
View File
@@ -2,11 +2,9 @@
import base64
import hashlib
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Cipher import AES
from Crypto.Cipher import ARC4
from Crypto.Cipher import AES, ARC4, PKCS1_v1_5
from Crypto.PublicKey import RSA
# 斗鱼RSA公钥(从JS中提取)
DOUYU_RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
+11 -14
View File
@@ -6,11 +6,9 @@ import re
import threading
import time
from datetime import datetime, timedelta
from typing import Optional
from loguru import logger
import requests
from loguru import logger
# Roundcube Webmail 地址。可通过环境变量覆盖。空值时回退到默认。
ROUNDCUBE_URL = os.getenv("MAIL_ROUNDCUBE_URL") or "http://111.229.206.54:8000/"
@@ -40,7 +38,7 @@ class EmailVerifier:
max_messages: int = 20,
use_ssl: bool = False,
roundcube_url: str = "",
backup_passwords: Optional[list[str]] = None,
backup_passwords: list[str] | None = None,
):
# 兼容旧参数名
self.imap_server = imap_server
@@ -54,14 +52,13 @@ class EmailVerifier:
)
# Roundcube 会话(懒初始化)
self._rc_session: Optional[requests.Session] = None
self._rc_token: Optional[str] = None
self._rc_session: requests.Session | None = None
self._rc_token: str | None = None
self._rc_logged_in = False
self._rc_login_error = ""
def connect(self) -> None:
"""兼容接口"""
pass
def disconnect(self) -> None:
"""关闭 Roundcube 会话"""
@@ -239,7 +236,7 @@ class EmailVerifier:
# ── Roundcube 读取邮件内容 ─────────────────────────────
def _rc_fetch_email_body(self, uid: int) -> Optional[str]:
def _rc_fetch_email_body(self, uid: int) -> str | None:
"""通过 Roundcube 读取指定 UID 邮件的正文"""
if not self._ensure_roundcube_session():
if self._rc_login_error:
@@ -262,7 +259,7 @@ class EmailVerifier:
# ── Roundcube 日期解析 ─────────────────────────────────
@staticmethod
def _parse_rc_date(date_str: str) -> Optional[datetime]:
def _parse_rc_date(date_str: str) -> datetime | None:
"""
解析 Roundcube 返回的日期字符串为 datetime。
@@ -363,9 +360,9 @@ class EmailVerifier:
self,
max_wait: int = 60,
interval: int = 3,
after_timestamp: Optional[float] = None,
after_timestamp: float | None = None,
allow_old_seconds: int = 15,
stop_event: Optional[threading.Event] = None,
stop_event: threading.Event | None = None,
) -> str:
"""
轮询获取斗鱼验证码。
@@ -417,9 +414,9 @@ class EmailVerifier:
def _fetch_code_via_roundcube(
self,
after_timestamp: Optional[float] = None,
after_timestamp: float | None = None,
allow_old_seconds: int = 15,
) -> Optional[str]:
) -> str | None:
"""通过 Roundcube API 获取最新斗鱼验证码"""
messages = self._rc_fetch_mail_list()
@@ -465,7 +462,7 @@ class EmailVerifier:
# ── 通用工具方法 ───────────────────────────────────────
def _extract_verification_code(self, text: str) -> Optional[str]:
def _extract_verification_code(self, text: str) -> str | None:
"""从文本中提取6位验证码"""
# 清理HTML标签和实体
text = html.unescape(text)
+26 -24
View File
@@ -4,28 +4,30 @@ import json
import re
import threading
import time
import requests
from typing import Mapping, Optional, Protocol, Tuple
from collections.abc import Mapping
from typing import Protocol
from urllib.parse import urlsplit, urlunsplit
import requests
from loguru import logger
from core.geetest.common.network import (
get_c_s,
get_js_address,
req_fullpage_validate,
)
from core.geetest.v3_slide.solver import (
_generate_seed,
get_w1,
get_w2,
)
from .cookie_enricher import CookieEnricher
from .email_verifier import EmailLoginError, EmailVerifier
from .login_api import LoginAPIStrategy
from .login_api_wgapi import WgapiLoginAPI
from .proxy_fetcher import ProxyFetcher
from core.geetest.v3_slide.solver import (
_generate_seed,
get_w1,
get_w2,
)
from core.geetest.common.network import (
get_js_address,
get_c_s,
req_fullpage_validate,
)
# ── 全局极验并发限制:同一时刻最多2个线程做极验验证 ──
_geetest_semaphore = threading.Semaphore(2)
@@ -95,14 +97,14 @@ class DouyuLogin:
def __init__(
self,
account: AccountLike,
proxy: Optional[str | Mapping[str, str]] = None,
proxy_api_url: Optional[str] = None,
proxy: str | Mapping[str, str] | None = None,
proxy_api_url: str | None = None,
timeout: tuple[float, float] = REQUEST_TIMEOUT,
max_login_retries: int = 0,
max_total_time: float = 0,
proxy_fetcher: Optional[ProxyFetcher] = None,
stop_event: Optional[threading.Event] = None,
api_strategy: Optional[LoginAPIStrategy] = None,
proxy_fetcher: ProxyFetcher | None = None,
stop_event: threading.Event | None = None,
api_strategy: LoginAPIStrategy | None = None,
):
self.account = account
self.proxy = proxy
@@ -117,7 +119,7 @@ class DouyuLogin:
ProxyFetcher(api_url=proxy_api_url) if proxy_api_url else None
)
self._current_proxy_url: Optional[str] = None
self._current_proxy_url: str | None = None
self._cookie_enrich_error = ""
self._static_retry_count = 0
self._setup_session()
@@ -565,7 +567,7 @@ class DouyuLogin:
)
return True
def _first_login(self) -> Tuple[str, str, str, dict]:
def _first_login(self) -> tuple[str, str, str, dict]:
"""
第一次登录,获取极验参数
@@ -608,7 +610,7 @@ class DouyuLogin:
def _solve_geetest(
self, gt: str, challenge: str, deadline: float = 0
) -> Tuple[str, str]:
) -> tuple[str, str]:
"""
解决极验 fullpage 验证(最多3次尝试,失败直接抛异常回到login换新代理)
@@ -635,7 +637,7 @@ class DouyuLogin:
def _solve_geetest_inner(
self, gt: str, challenge: str, deadline: float = 0
) -> Tuple[str, str]:
) -> tuple[str, str]:
"""极验验证内部实现:最多3次尝试,失败直接抛异常。"""
_MAX_ATTEMPTS = 3
@@ -652,7 +654,7 @@ class DouyuLogin:
# 超时兜底
if deadline and time.monotonic() > deadline:
raise ValueError(f"极验验证超时(登录整体时间耗尽)")
raise ValueError("极验验证超时(登录整体时间耗尽)")
try:
logger.info(f"极验验证尝试 {attempt}/{_MAX_ATTEMPTS}")
@@ -825,7 +827,7 @@ class DouyuLogin:
logger.info("验证邮件已发送")
def _get_email_code(self, after_timestamp: Optional[float] = None) -> str:
def _get_email_code(self, after_timestamp: float | None = None) -> str:
"""获取邮箱验证码"""
verifier = EmailVerifier(
imap_server=self.account.email_imap_server,
+1 -2
View File
@@ -1,7 +1,6 @@
"""登录接口策略基类"""
from abc import ABC, abstractmethod
from typing import Tuple
class LoginAPIStrategy(ABC):
@@ -78,7 +77,7 @@ class LoginAPIStrategy(ABC):
"""构建提交验证码参数"""
...
def extract_geetest_params(self, payload: dict) -> Tuple[str, str, str]:
def extract_geetest_params(self, payload: dict) -> tuple[str, str, str]:
"""从第一次登录响应提取极验参数
Args:
+1
View File
@@ -10,6 +10,7 @@
import threading
import time
from loguru import logger
from .proxy_resolver import ProxyResolver
+1 -2
View File
@@ -2,10 +2,9 @@
import json
import re
from typing import Optional
def parse_proxy_response(text: str) -> tuple[list[str], Optional[str]]:
def parse_proxy_response(text: str) -> tuple[list[str], str | None]:
"""
解析代理 API 响应,支持 JSON 格式与旧版纯文本格式。
+2 -2
View File
@@ -13,7 +13,7 @@ from .xiequ import XiequAdapter
from .xkdaili import XkdailiAdapter
# ── 平台注册表 ──
_PLATFORM_REGISTRY: dict[str, Type[BaseWhitelistAdapter]] = {
_PLATFORM_REGISTRY: dict[str, type[BaseWhitelistAdapter]] = {
"xiequ": XiequAdapter,
"xkdaili": XkdailiAdapter,
}
@@ -49,7 +49,7 @@ _PLATFORM_LABELS: dict[str, str] = {
}
def create_adapter(platform: str, credentials: dict) -> Optional[BaseWhitelistAdapter]:
def create_adapter(platform: str, credentials: dict) -> BaseWhitelistAdapter | None:
"""根据平台标识符和凭据创建适配器实例。"""
cls = _PLATFORM_REGISTRY.get(platform)
if not cls:
+3 -4
View File
@@ -8,7 +8,6 @@ import re
import threading
import time
from abc import ABC, abstractmethod
from typing import Optional
import requests
from loguru import logger
@@ -144,12 +143,12 @@ class BaseWhitelistAdapter(ABC):
return False, msg
@staticmethod
def get_local_exit_ip() -> Optional[str]:
def get_local_exit_ip() -> str | None:
"""获取本机当前公网出口IP(不走代理)。"""
return _get_local_exit_ip()
def _get_local_exit_ip() -> Optional[str]:
def _get_local_exit_ip() -> str | None:
"""获取本机当前公网出口IP(不走代理)。"""
targets = [
"https://myip.ipip.net",
@@ -171,7 +170,7 @@ def _get_local_exit_ip() -> Optional[str]:
return None
def get_exit_ip_via_proxy(proxy: str) -> Optional[str]:
def get_exit_ip_via_proxy(proxy: str) -> str | None:
"""通过代理获取出口IP。"""
targets = [
"https://qifu-api.baidubce.com/ip/local/geo/v1/district",
+10 -9
View File
@@ -2,7 +2,8 @@
import threading
import time
from typing import Callable, Optional, Protocol
from collections.abc import Callable
from typing import Protocol
import requests
from loguru import logger
@@ -19,7 +20,7 @@ class WhitelistSyncer(Protocol):
def sync_ip(self, ip: str) -> tuple[bool, str]: ...
def get_local_exit_ip(self) -> Optional[str]: ...
def get_local_exit_ip(self) -> str | None: ...
class ProxyResolver:
@@ -28,11 +29,11 @@ class ProxyResolver:
def __init__(
self,
api_url: str,
whitelist_syncer: Optional[WhitelistSyncer] = None,
log_func: Optional[LogFunc] = None,
whitelist_syncer: WhitelistSyncer | None = None,
log_func: LogFunc | None = None,
sync_local_exit_ip: bool = False,
sync_whitelist_once: bool = True,
stop_event: Optional[threading.Event] = None,
stop_event: threading.Event | None = None,
):
self.api_url = api_url
self.whitelist_syncer = whitelist_syncer
@@ -40,7 +41,7 @@ class ProxyResolver:
self.sync_local_exit_ip = sync_local_exit_ip
self.sync_whitelist_once = sync_whitelist_once
self.stop_event = stop_event
self._last_synced_ip: Optional[str] = None
self._last_synced_ip: str | None = None
self._has_synced_whitelist = False
def _is_stopped(self) -> bool:
@@ -95,7 +96,7 @@ class ProxyResolver:
self,
max_attempts: int = 4,
return_all: bool = False,
) -> tuple[Optional[str | list[str]], str]:
) -> tuple[str | list[str] | None, str]:
"""
获取并验证代理。
@@ -178,8 +179,8 @@ def resolve_working_proxy(
whitelist_platform: str = "xiequ",
whitelist_credentials: dict | None = None,
max_attempts: int = 4,
log_func: Optional[LogFunc] = None,
) -> tuple[Optional[str], str]:
log_func: LogFunc | None = None,
) -> tuple[str | None, str]:
"""从代理 API 获取可用代理,自动处理白名单同步。"""
syncer = (
DouyuWhitelistSyncer(
+1 -2
View File
@@ -1,7 +1,6 @@
"""代理可用性验证。"""
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Optional
import requests
from loguru import logger
@@ -45,7 +44,7 @@ def verify_proxies_concurrent(
timeout: tuple = (5, 8),
max_workers: int = 5,
return_all: bool = False,
) -> tuple[Optional[str | list[str]], str]:
) -> tuple[str | list[str] | None, str]:
"""
并发验证多个代理 URL。
+3 -4
View File
@@ -1,6 +1,5 @@
"""代理模块使用的白名单适配器。"""
from typing import Optional
from .proxy_platforms import create_adapter
from .proxy_platforms.base import BaseWhitelistAdapter, _get_local_exit_ip
@@ -17,7 +16,7 @@ class DouyuWhitelistSyncer:
def __init__(
self,
platform: str = "xiequ",
credentials: Optional[dict] = None,
credentials: dict | None = None,
uid: str = "",
ukey: str = "",
):
@@ -28,7 +27,7 @@ class DouyuWhitelistSyncer:
self.platform = platform
self.credentials = credentials or {}
self._adapter: Optional[BaseWhitelistAdapter] = None
self._adapter: BaseWhitelistAdapter | None = None
if self.credentials:
self._adapter = create_adapter(platform, self.credentials)
@@ -38,5 +37,5 @@ class DouyuWhitelistSyncer:
return False, "未配置白名单凭据"
return self._adapter.sync_ip(ip)
def get_local_exit_ip(self) -> Optional[str]:
def get_local_exit_ip(self) -> str | None:
return _get_local_exit_ip()
+7 -6
View File
@@ -6,9 +6,10 @@ import hashlib
import json
import os
import time
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
from typing import Any, Callable, Mapping
from typing import Any
import requests
@@ -34,7 +35,7 @@ class FishFinRechargeConfig:
debug: bool = False
@classmethod
def from_env(cls) -> "FishFinRechargeConfig":
def from_env(cls) -> FishFinRechargeConfig:
"""从环境变量读取配置,不在代码或数据库中保存商户密钥。"""
timeout = float(os.getenv("FISH_FIN_RECHARGE_TIMEOUT", "20"))
return cls(
@@ -189,7 +190,7 @@ class FishFinRechargeClient:
"sign_params": sign_params,
# 不把可重放签名原文或 AppSecret 写入日志,只记录待签名串摘要。
"sign_source_digest": hashlib.sha256(
f"{sign_query}{method.upper()}".encode("utf-8")
f"{sign_query}{method.upper()}".encode()
).hexdigest()[:12],
}
)
@@ -240,7 +241,7 @@ class FishFinRechargeClient:
return payload
@staticmethod
def _amount(value: Decimal | int | float | str) -> str:
def _amount(value: Decimal | float | str) -> str:
"""规范化金额,避免浮点数表达式进入签名或订单请求。"""
try:
price = Decimal(str(value))
@@ -251,7 +252,7 @@ class FishFinRechargeClient:
return format(price.normalize(), "f")
@classmethod
def _json_amount(cls, value: Decimal | int | float | str) -> int | float:
def _json_amount(cls, value: Decimal | float | str) -> int | float:
"""按文档以 JSON 数字发送金额,整数不附带无意义的小数位。"""
amount_text = cls._amount(value)
return int(amount_text) if "." not in amount_text else float(amount_text)
@@ -267,7 +268,7 @@ class FishFinRechargeClient:
self,
*,
buy_num: int,
pay_amount: Decimal | int | float | str,
pay_amount: Decimal | float | str,
out_order_id: str,
product_id: str,
recharge_arg: list[dict[str, Any]],
+3 -2
View File
@@ -1,8 +1,9 @@
import random
import hashlib
import random
from typing import Any
from Crypto.Cipher import AES, PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5, AES
# 随机产生4个字符组成的字符串
+2 -2
View File
@@ -1,8 +1,8 @@
import requests
import cv2
import numpy as np
from PIL import Image
import requests
from loguru import logger
from PIL import Image
REQUEST_TIMEOUT = (3.05, 12)
+13 -12
View File
@@ -1,8 +1,9 @@
import time
import requests
import json
import re
from typing import Mapping, Optional, Tuple
import time
from collections.abc import Mapping
import requests
from loguru import logger
REQUEST_TIMEOUT = (10, 30)
@@ -12,9 +13,9 @@ PASSPORT_REFERER = "https://passport.douyu.com/"
def _get(
url: str,
*,
params: Optional[dict] = None,
headers: Optional[dict] = None,
proxies: Optional[Mapping[str, str]] = None,
params: dict | None = None,
headers: dict | None = None,
proxies: Mapping[str, str] | None = None,
) -> requests.Response:
"""发送极验 GET 请求,确保使用同一个代理出口。"""
return requests.get(
@@ -55,7 +56,7 @@ def _parse_json_response(response: requests.Response, source: str) -> dict:
raise ValueError(f"{source} 返回的不是有效 JSON: {preview}") from exc
def get_challenge_gt_bak() -> Tuple[str, str]:
def get_challenge_gt_bak() -> tuple[str, str]:
headers = {
"accept": "application/json, text/javascript, */*; q=0.01",
"accept-language": "zh-CN,zh;q=0.9",
@@ -85,7 +86,7 @@ def get_challenge_gt_bak() -> Tuple[str, str]:
return data["gt"], data["challenge"]
def get_challenge_gt() -> Tuple[str, str]:
def get_challenge_gt() -> tuple[str, str]:
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
"Accept": "application/json, text/javascript, */*; q=0.01",
@@ -116,7 +117,7 @@ def get_challenge_gt() -> Tuple[str, str]:
raise ValueError(f"斗鱼登录接口返回中缺少极验参数: {preview}") from exc
def get_js_address(gt: str, proxies: Optional[Mapping[str, str]] = None) -> dict:
def get_js_address(gt: str, proxies: Mapping[str, str] | None = None) -> dict:
headers = {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
@@ -149,8 +150,8 @@ def get_c_s(
gt: str,
challenge: str,
w: str,
proxies: Optional[Mapping[str, str]] = None,
) -> Tuple[list[int], str]:
proxies: Mapping[str, str] | None = None,
) -> tuple[list[int], str]:
headers = {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
@@ -184,7 +185,7 @@ def req_fullpage_validate(
gt: str,
challenge: str,
w: str,
proxies: Optional[Mapping[str, str]] = None,
proxies: Mapping[str, str] | None = None,
) -> dict:
"""HAR 中的 fullpage 最终校验,成功后直接返回 validate。"""
headers = {
+2 -3
View File
@@ -1,9 +1,8 @@
import time
import random
from typing import Optional
import time
def generate_fake_performance_timing(base_time: Optional[int] = None) -> dict[str, int]:
def generate_fake_performance_timing(base_time: int | None = None) -> dict[str, int]:
"""
生成伪造的浏览器性能时间戳数据
+3 -3
View File
@@ -1,6 +1,6 @@
import random
import math
from typing import Any, Optional
import random
from typing import Any
# 生成类人的鼠标轨迹
@@ -91,7 +91,7 @@ def generate_realistic_trajectory(
# 处理原始轨迹数组
def process_mouse_trajectory(
events: list[Any], max_records: Optional[int] = None
events: list[Any], max_records: int | None = None
) -> dict[str, Any]:
"""
处理鼠标/触摸轨迹数据,将绝对坐标转换为相对坐标和时间差
+16 -14
View File
@@ -1,35 +1,37 @@
import time
import random
import json
import random
import time
from loguru import logger
from core.geetest.common.trajectory import (
generate_realistic_trajectory,
process_mouse_trajectory,
compress_trajectory,
TrajectoryEncoder,
H,
)
from core.geetest.common.crypto import (
four_random_chart,
RSA_jiami_r,
AES_O,
geetest_base64_encode,
RSA_jiami_r,
encrypt_string,
four_random_chart,
geetest_base64_encode,
simple_md5,
)
from core.geetest.common.imaging import download_picture
from core.geetest.common.network import (
get_c_s,
get_challenge_gt,
get_js_address,
get_c_s,
req_slide,
get_picture,
req_end,
req_slide,
)
from core.geetest.common.performance import (
generate_fake_performance_timing,
get_slide_track,
)
from core.geetest.common.trajectory import (
H,
TrajectoryEncoder,
compress_trajectory,
generate_realistic_trajectory,
process_mouse_trajectory,
)
def _generate_seed() -> str:
+7 -7
View File
@@ -33,23 +33,23 @@ if TYPE_CHECKING:
)
__all__ = [
"HuyaHttpClient",
"HuyaWssClient",
"GetUserScoreReq",
"GetUserScoreResp",
"HuyaAppLoginError",
"HuyaAppPasswordLogin",
"HuyaAppQrAuthRequiredError",
"HuyaCredentialError",
"HuyaHttpClient",
"HuyaLoginError",
"HuyaLoginResult",
"HuyaPasswordLogin",
"HuyaAppLoginError",
"HuyaAppQrAuthRequiredError",
"HuyaAppPasswordLogin",
"HuyaSmsCodeResult",
"HuyaSmsLogin",
"HuyaVerificationError",
"HuyaVerificationSolver",
"login_huya_password",
"HuyaWssClient",
"login_huya_app_password",
"login_huya_password",
"login_huya_sms",
"send_huya_sms_code",
"solve_huya_verification",
@@ -98,8 +98,8 @@ def __getattr__(name: str):
}:
from .app_login import (
HuyaAppLoginError,
HuyaAppQrAuthRequiredError,
HuyaAppPasswordLogin,
HuyaAppQrAuthRequiredError,
login_huya_app_password,
)
+1 -2
View File
@@ -35,13 +35,12 @@ import os
import sys
import time
from .app_login import HuyaAppPasswordLogin
from .device_profile import (
HDID32, # 登录帧 t1.t0 32hex 常量 (R15 公式结果, k1 来源见 R39; device_profile.py)
_load_db,
_save_db,
get_profile, # 幂等画像: 同账号永远复用同一套 (data/huya_device_profiles.json)
)
from .app_login import HuyaAppPasswordLogin
# ---------------------------------------------------------------------------
+2 -2
View File
@@ -130,7 +130,7 @@ def wup_password_login_raw(
固定 action/device_id。风控重试调用方应显式复用同一注册结果。
注册链失败抛 ``HuyaAppLoginError``,绝不静默回退旧固定值。
"""
uid_str = account[3:] if account.startswith("hy_") else account
uid_str = account.removeprefix("hy_")
dev = dict(device_info) if device_info is not None else get_profile(account)
mj, ua, _old_sd = _golden_session_assets()
if not safedeviceid:
@@ -304,7 +304,7 @@ def login_cred_with_flow(
logger.info(f"[huya-app] 第 {rnd + 1} 轮触发安全验证: {kind}")
if "qr_auth" in risk_url:
raise HuyaAppQrAuthRequiredError(
f"该账号 App 渠道要求扫码验证(qr_auth),请先在手机虎牙 App 上正常登录一次建立设备信任。"
"该账号 App 渠道要求扫码验证(qr_auth),请先在手机虎牙 App 上正常登录一次建立设备信任。"
)
solve_safe_auth(risk_url, proxies=proxies, device_info=dev)
logger.info("[huya-app] safe_auth 滑块过验成功,重发 WUP 登录...")
-1
View File
@@ -5,7 +5,6 @@
from __future__ import annotations
import base64
import os
import struct
-1
View File
@@ -30,7 +30,6 @@ from .login import (
generate_request_id,
)
CHANGE_PASSWORD_VERSION = "2.5"
CHANGE_PASSWORD_CHECK_URI = "60011"
CHANGE_PASSWORD_SEND_SMS_URI = "60003"
-1
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Iterable, Mapping
import requests
from requests.cookies import RequestsCookieJar
+5 -5
View File
@@ -116,7 +116,7 @@ class Envelope:
self._parse()
@classmethod
def load(cls, path: str | Path | None = None) -> "Envelope":
def load(cls, path: str | Path | None = None) -> Envelope:
"""加载信封模板,支持从文件加载或使用内嵌金样本。"""
if path:
p = Path(path)
@@ -136,7 +136,7 @@ class Envelope:
return cls(base64.b64decode(DEFAULT_QURL_B64))
@classmethod
def _load_from_path(cls, p: Path) -> "Envelope":
def _load_from_path(cls, p: Path) -> Envelope:
if p.suffix == ".json":
j = json.loads(p.read_text("utf-8"))
q = next(
@@ -236,7 +236,7 @@ class Envelope:
assert self.uid_off is not None
return struct.unpack_from(">Q", self.raw, self.uid_off)[0]
def patch_uid(self, uid: int) -> "Envelope":
def patch_uid(self, uid: int) -> Envelope:
assert self.uid_off is not None
struct.pack_into(">Q", self.raw, self.uid_off, uid)
return self
@@ -246,7 +246,7 @@ class Envelope:
assert self.cert_off is not None
return bytes(self.raw[self.cert_off : self.cert_off + self.cert_len])
def patch_cert(self, cert: bytes) -> "Envelope":
def patch_cert(self, cert: bytes) -> Envelope:
assert self.cert_off is not None
b64 = base64.b64encode(cert)
if len(b64) != self.cert_len:
@@ -256,7 +256,7 @@ class Envelope:
self.raw[self.cert_off : self.cert_off + self.cert_len] = b64
return self
def patch_session(self, session: int) -> "Envelope":
def patch_session(self, session: int) -> Envelope:
if self.tag4_span:
struct.pack_into(">I", self.raw, self.tag4_span[0], session & 0xFFFFFFFF)
d = self.raw
+3 -8
View File
@@ -3,6 +3,7 @@ TAF/WUP 帧解码器 — 将二进制帧转为可读摘要,用于日志输出
"""
from typing import Any, cast
from .taf_protocol import TafInputStream, TafType
from .wup_protocol import normalize_wup_payload
@@ -103,7 +104,7 @@ def _decode_taf_struct(ins: TafInputStream, depth: int = 0) -> dict:
except Exception:
val = f"<decode_err:0x{dtype:02x}>"
else:
val = f"<...>"
val = "<...>"
try:
ins.skip_field(dtype)
except Exception:
@@ -152,13 +153,7 @@ def _decode_wup_body(body: bytes) -> dict:
if tag > 10:
break
ins.read_head()
if tag == 1:
if dtype != TafType.ZERO:
ins._read_int_value(dtype)
elif tag in (2, 3):
if dtype != TafType.ZERO:
ins._read_int_value(dtype)
elif tag == 4:
if tag == 1 or tag in (2, 3) or tag == 4:
if dtype != TafType.ZERO:
ins._read_int_value(dtype)
elif tag == 5:
+7 -7
View File
@@ -11,13 +11,13 @@ import base64
import hashlib
import json
import random
import struct
import urllib.parse
import urllib.request
from typing import Any, Optional, Callable
from collections.abc import Callable
from typing import Any
from .cookie_utils import cookie_pairs, normalize_cookie_pairs, normalize_huya_cookie
from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct
from .wup_protocol import WupRequest, WupResponse
CDNWS_HOST = "cdnws.api.huya.com"
@@ -849,11 +849,11 @@ class HuyaHttpClient:
):
"""order_type=None 时自动尝试从 1 到 10 找到有效值"""
from .shop_structs import (
CreateOrderReqV5,
CreateOrderRsp,
CreateOrderAccountParam,
CreateOrderExtraParam,
CreateOrderPromotionParam,
CreateOrderAccountParam,
CreateOrderReqV5,
CreateOrderRsp,
)
# 如果指定了具体值,直接试
@@ -951,7 +951,7 @@ class HuyaHttpClient:
baseinfo = self._generate_rpc_baseinfo(uid, guid, cookie)
url = f"https://{CDNWS_HOST}/?baseinfo={baseinfo}"
self.logger(f"[HTTP] POST shopMiddleUI.payOrderSubmitV5")
self.logger("[HTTP] POST shopMiddleUI.payOrderSubmitV5")
self.logger(f"[HTTP] 发送 hex前60: {wup_data[:60].hex()}")
req = urllib.request.Request(
+2 -3
View File
@@ -14,12 +14,11 @@ from http.cookies import SimpleCookie
from urllib.parse import quote, urlsplit, urlunsplit
import requests
from requests.cookies import RequestsCookieJar
from loguru import logger
from requests.cookies import RequestsCookieJar
from .cookie_utils import normalize_huya_cookie
APP_ID = "5002"
APP_VERSION = "2.6"
APP_SIGN = "1ce3bf682483d03f146f58232ec10635"
@@ -101,7 +100,7 @@ def generate_context(device_id: str | None = None, mid: str | None = None) -> st
def generate_request_id() -> str:
"""生成 requestId,形态参考旧实现的日内毫秒数。"""
now = dt.datetime.now(dt.timezone.utc)
now = dt.datetime.now(dt.UTC)
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
return str(int((now - midnight).total_seconds() * 1000))
+14 -14
View File
@@ -5,8 +5,8 @@
来源:m-shop.yaoguo.com 的 jce/ShopFacade.js、api/orderui.ts、api/mall/PlayMallNewHome.ts
"""
from typing import List, Dict, Optional
from .taf_protocol import TafOutputStream, TafInputStream, TafStruct, TafType
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType
class OrderType:
@@ -387,7 +387,7 @@ class GoodsPriceInfo(TafStruct):
self.spuId: str = ""
self.minPrice: int = 0
self.maxPrice: int = 0
self.skuMap: Dict[int, GoodsSkuItem] = {}
self.skuMap: dict[int, GoodsSkuItem] = {}
self.stock: int = 0
self.buyLimit: int = 0
self.price: int = 0
@@ -500,7 +500,7 @@ class OrderListGoodsDetail(TafStruct):
self.buyerUid: int = 0 # tag 18
self.virtualType: int = 0 # tag 19
self.quantity: int = 0 # tag 20
self.shopInfo: Optional[OrderListShopInfo] = None # tag 21
self.shopInfo: OrderListShopInfo | None = None # tag 21
self.points: int = 0 # tag 23
def read_from(self, ins: TafInputStream):
@@ -544,7 +544,7 @@ class OrderListItem(TafStruct):
self.totalPrice: int = 0 # tag 12 分
self.createTime: int = 0 # tag 14 毫秒时间戳
self.payTime: int = 0 # tag 15 毫秒时间戳
self.goodsDetail: Optional[OrderListGoodsDetail] = None # tag 16
self.goodsDetail: OrderListGoodsDetail | None = None # tag 16
def read_from(self, ins: TafInputStream):
self.bizOrderId = ins.read_string(0, default=self.bizOrderId)
@@ -611,7 +611,7 @@ class QueryUserOrderListRsp(TafStruct):
self.code: int = 0
self.message: str = ""
self.totalCount: int = 0
self.orders: List[OrderListItem] = []
self.orders: list[OrderListItem] = []
@staticmethod
def _read_order_item(ins: TafInputStream, _tag: int):
@@ -692,9 +692,9 @@ class CreateOrderPromotionParam(TafStruct):
class CreateOrderAccountParam(TafStruct):
def __init__(self):
self.payoutTypeList: List[int] = [] # tag 0 Vector<INT32>
self.payoutTypeList: list[int] = [] # tag 0 Vector<INT32>
self.payoutChargeAmount: int = 0 # tag 1
self.cancelPayoutTypeList: List[int] = [] # tag 2
self.cancelPayoutTypeList: list[int] = [] # tag 2
self.recycleSupplierId: int = 0 # tag 3
self.claimPrice: int = 0 # tag 4
@@ -739,20 +739,20 @@ class CreateOrderReqV5(TafStruct):
self.gameId: str = "" # tag 8
self.orderId: int = 0 # tag 9
self.src: int = 0 # tag 10
self.couponUserIds: List[int] = [] # tag 11 Vector<INT64>
self.couponUserIds: list[int] = [] # tag 11 Vector<INT64>
self.orderType: int = 0 # tag 12
self.extraParam: Optional[CreateOrderExtraParam] = None # tag 13
self.extraParam: CreateOrderExtraParam | None = None # tag 13
self.scene: int = 0 # tag 14
self.promotionItems: List = [] # tag 15 Vector<PromotionItem>
self.promotionItems: list = [] # tag 15 Vector<PromotionItem>
self.sourceId: str = "" # tag 16
self.env: Dict[str, str] = {} # tag 17 Map<STRING,STRING>
self.env: dict[str, str] = {} # tag 17 Map<STRING,STRING>
self.orderScene: int = 0 # tag 18
self.watchWord: str = "" # tag 19
self.marketingChannel: str = "" # tag 20
self.promotionParam: Optional[CreateOrderPromotionParam] = None # tag 21
self.promotionParam: CreateOrderPromotionParam | None = None # tag 21
self.externalTraceKey: str = "" # tag 22
self.kefuUid: int = 0 # tag 23
self.accountParam: Optional[CreateOrderAccountParam] = None # tag 24
self.accountParam: CreateOrderAccountParam | None = None # tag 24
self.parentOrderId: int = 0 # tag 25
self.vendorAccountType: str = "" # tag 26
self.vendorAccountVal: str = "" # tag 27
+1 -2
View File
@@ -29,7 +29,6 @@ from .login import (
generate_request_id,
)
SMS_CODE_URI = "60027"
SMS_LOGIN_URI = "60025"
SMS_CODE_URL = "https://udblgn.huya.com/web/v2/smsCode"
@@ -604,7 +603,7 @@ class HuyaSmsLogin:
phone: str = "",
proxies: Mapping[str, str] | None = None,
timeout: tuple[float, float] | None = None,
) -> "HuyaSmsLogin":
) -> HuyaSmsLogin:
"""从发码阶段返回的 state 恢复短信登录会话。"""
try:
raw = base64.urlsafe_b64decode(state.encode("ascii"))
+10 -10
View File
@@ -9,9 +9,9 @@
0x0c ZERO 0x0d SIMPLE_LIST
"""
import struct
import io
from typing import Any, Dict, List, Optional, Tuple
import struct
from typing import Any
class TafType:
@@ -136,7 +136,7 @@ class TafOutputStream:
# ---- Map ----
def write_map(
self, tag: int, value: Dict[Any, Any], key_writer=None, val_writer=None
self, tag: int, value: dict[Any, Any], key_writer=None, val_writer=None
):
self.write_head(tag, TafType.MAP)
self.write_int32(0, len(value))
@@ -151,7 +151,7 @@ class TafOutputStream:
self._write_any(1, v)
# ---- List ----
def write_list(self, tag: int, value: List[Any], item_writer=None):
def write_list(self, tag: int, value: list[Any], item_writer=None):
self.write_head(tag, TafType.LIST)
self.write_int32(0, len(value))
for item in value:
@@ -192,7 +192,7 @@ class TafInputStream:
def __init__(self, data: bytes):
self.buf = io.BytesIO(data)
def peek_head(self) -> Tuple[int, int]:
def peek_head(self) -> tuple[int, int]:
"""读取 head 但不消费(用于探测)"""
pos = self.buf.tell()
try:
@@ -200,7 +200,7 @@ class TafInputStream:
finally:
self.buf.seek(pos)
def read_head(self) -> Tuple[int, int]:
def read_head(self) -> tuple[int, int]:
"""返回 (tag, type)"""
data = self.buf.read(1)
if not data:
@@ -293,7 +293,7 @@ class TafInputStream:
self.skip_field(it)
# ---- 带跳过策略的字段读取:找到 tag,否则返回默认 ----
def _find_tag(self, target_tag: int, required: bool) -> Optional[Tuple[int, int]]:
def _find_tag(self, target_tag: int, required: bool) -> tuple[int, int] | None:
"""逐个读 head,tag 相等则返回,tag 超过则回退并返回 None"""
while True:
pos = self.buf.tell()
@@ -402,7 +402,7 @@ class TafInputStream:
# ---- 复合类型 ----
def read_map(
self, tag: int, required: bool = False, key_reader=None, val_reader=None
) -> Dict:
) -> dict:
found = self._find_tag(tag, required)
if not found:
return {}
@@ -418,7 +418,7 @@ class TafInputStream:
result[k] = v
return result
def read_list(self, tag: int, required: bool = False, item_reader=None) -> List:
def read_list(self, tag: int, required: bool = False, item_reader=None) -> list:
found = self._find_tag(tag, required)
if not found:
return []
@@ -486,7 +486,7 @@ class TafStruct:
def read_from(self, ins: TafInputStream):
raise NotImplementedError
def to_dict(self) -> Dict[str, Any]:
def to_dict(self) -> dict[str, Any]:
"""调试用:转字典"""
return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
-1
View File
@@ -13,7 +13,6 @@ import onnxruntime as ort
from PIL import Image
from scipy.optimize import linear_sum_assignment
MODEL_DIR = Path(__file__).resolve().parent / "models"
+1 -2
View File
@@ -17,13 +17,12 @@ import cv2
import execjs
import numpy as np
import requests
from PIL import Image
from loguru import logger
from PIL import Image
from .ocr import HuyaCaptchaOcr, default_ocr
from .track import format_track, generate_slide_track
DEFAULT_UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
+12 -12
View File
@@ -21,14 +21,14 @@ import re
import struct
import time
from collections import deque
from typing import Any, Optional, Callable, cast
from collections.abc import Callable
from typing import Any, cast
import websockets
from .frame_decoder import _decode_taf_struct, _truncate, format_wss_log
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType
from .wup_protocol import WupRequest, WupResponse
from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct
from .frame_decoder import format_wss_log, _decode_taf_struct, _truncate
SHOP_WS_HOST = "77bc035c-ws.va.huya.com"
# 商城端点 baseinfo (conn4, h5_/index.html) — 商城业务 shopMiddleUI 走此通道
@@ -225,7 +225,7 @@ class HuyaWssClient:
),
timeout=timeout,
)
except asyncio.TimeoutError:
except TimeoutError:
self.logger(f"[WSS] 连接超时({timeout}s")
raise
except Exception as e:
@@ -376,7 +376,7 @@ class HuyaWssClient:
try:
body = await asyncio.wait_for(future, timeout)
except asyncio.TimeoutError:
except TimeoutError:
if future in self.rpc_queue:
self.rpc_queue.remove(future)
self.logger("[✗] wsLaunch 超时")
@@ -526,7 +526,7 @@ class HuyaWssClient:
try:
body = await asyncio.wait_for(future, timeout)
except asyncio.TimeoutError:
except TimeoutError:
if future in self.rpc_queue:
self.rpc_queue.remove(future)
self.logger("[✗] getConfig 超时")
@@ -596,7 +596,7 @@ class HuyaWssClient:
try:
body = await asyncio.wait_for(future, timeout)
except asyncio.TimeoutError:
except TimeoutError:
if future in self.rpc_queue:
self.rpc_queue.remove(future)
self.logger(f"[✗] {service}.{method} 超时")
@@ -696,11 +696,11 @@ class HuyaWssClient:
order_type: int = 6,
):
from .shop_structs import (
CreateOrderReqV5,
CreateOrderRsp,
CreateOrderAccountParam,
CreateOrderExtraParam,
CreateOrderPromotionParam,
CreateOrderAccountParam,
CreateOrderReqV5,
CreateOrderRsp,
)
req = CreateOrderReqV5()
@@ -781,7 +781,7 @@ class HuyaWssClient:
try:
body = await asyncio.wait_for(future, 15.0)
except asyncio.TimeoutError:
except TimeoutError:
if future in self.rpc_queue:
self.rpc_queue.remove(future)
self.logger("[✗] payOrderSubmitV5 超时")
+4 -4
View File
@@ -9,7 +9,7 @@ import json
import random
import struct
import time as _time
from typing import Any, Dict
from typing import Any
# TAF 类型标签
INT8, INT16, INT32, INT64 = 0x00, 0x01, 0x02, 0x03
@@ -97,7 +97,7 @@ class _Writer:
def _build_meta_json(session: int, trace_id: str) -> str:
"""构造 _wup_data.t0.t2 元数据 JSON。"""
meta: Dict[str, Any] = {
meta: dict[str, Any] = {
"associationId": 8193,
"funcName": "hypasswordLogin",
"group": 1,
@@ -167,7 +167,7 @@ def _build_wup_data(
session: int,
trace_id: str,
user_action_json: str,
device_info: Dict[str, str],
device_info: dict[str, str],
) -> None:
"""编码 _wup_data struct。"""
meta_json = _build_meta_json(session, trace_id)
@@ -235,7 +235,7 @@ def build_password_login_wup(
session: int,
trace_id: str,
user_action_json: str,
device_info: Dict[str, str],
device_info: dict[str, str],
) -> bytes:
"""构造密码登录的 WUP TAF 请求体。"""
wd = _Writer()
+10 -9
View File
@@ -11,8 +11,9 @@ Wup 包结构:
"""
import struct
from typing import Any, Dict, Optional
from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct
from typing import Any
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType
class WupRequest:
@@ -27,9 +28,9 @@ class WupRequest:
self.sFuncName: str = "" # tag 6
self.sBuffer: bytes = b"" # tag 7
self.iTimeout: int = 3000 # tag 8
self.context: Dict[str, str] = {} # tag 9
self.status: Dict[str, str] = {} # tag 10
self.newdata: Dict[str, bytes] = {}
self.context: dict[str, str] = {} # tag 9
self.status: dict[str, str] = {} # tag 10
self.newdata: dict[str, bytes] = {}
def setServant(self, name: str):
self.sServantName = name
@@ -141,9 +142,9 @@ class WupResponse:
self.sFuncName: str = ""
self.sBuffer: bytes = b""
self.iTimeout: int = 0
self.context: Dict[str, str] = {}
self.status: Dict[str, str] = {}
self.newdata: Dict[str, bytes] = {}
self.context: dict[str, str] = {}
self.status: dict[str, str] = {}
self.newdata: dict[str, bytes] = {}
def decode(self, data: bytes):
"""解码响应(不包含长度前缀;若含前缀会自动跳过)"""
@@ -269,7 +270,7 @@ def _read_bytes_value(ins: TafInputStream, dtype: int) -> bytes:
return ins.buf.read(length)
def _read_map_value(ins: TafInputStream, dtype: int) -> Dict:
def _read_map_value(ins: TafInputStream, dtype: int) -> dict:
if dtype != TafType.MAP:
return {}
count = ins._read_int_len()
-1
View File
@@ -10,7 +10,6 @@ from urllib.parse import urlparse
import requests
CODE_PATTERN = re.compile(
r"(?:verification\s+code|验证码|校验码|动态码|安全码)\D{0,20}(\d{4,8})",
re.IGNORECASE,