执行 Ruff 安全自动修复
This commit is contained in:
+10
-10
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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,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-----
|
||||
|
||||
@@ -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
@@ -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,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:
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from .proxy_resolver import ProxyResolver
|
||||
|
||||
@@ -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 格式与旧版纯文本格式。
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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。
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]],
|
||||
|
||||
@@ -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个字符组成的字符串
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
生成伪造的浏览器性能时间戳数据
|
||||
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
处理鼠标/触摸轨迹数据,将绝对坐标转换为相对坐标和时间差
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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 登录...")
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import os
|
||||
import struct
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Mapping
|
||||
|
||||
import requests
|
||||
from requests.cookies import RequestsCookieJar
|
||||
|
||||
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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("_")}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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 超时")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -14,7 +14,6 @@ from sqlalchemy import JSON, MetaData, create_engine, func, inspect, select
|
||||
from sqlalchemy.engine import Connection, Engine
|
||||
from sqlalchemy.schema import Table
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_SOURCE = PROJECT_ROOT / "data" / "web.db"
|
||||
IGNORED_SOURCE_TABLES = {"alembic_version"}
|
||||
@@ -147,7 +146,7 @@ def main() -> int:
|
||||
if target_url:
|
||||
os.environ["DATABASE_URL"] = target_url
|
||||
from web.backend import models # noqa: F401
|
||||
from web.backend.database import Base, DATABASE_URL, run_migrations
|
||||
from web.backend.database import DATABASE_URL, Base, run_migrations
|
||||
|
||||
target_url = target_url or DATABASE_URL
|
||||
if not target_url.startswith("mysql+"):
|
||||
|
||||
@@ -34,19 +34,20 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from pyvm.algorithm import build_plaintext, generate_encrypt_msg_offline # noqa: E402
|
||||
from pyvm.mall import MallSession, generate_encrypt_msg as mall_generate # noqa: E402
|
||||
from pyvm.session import SessionState, load_session # noqa: E402
|
||||
from pyvm.login_profile import midas_login_params # noqa: E402
|
||||
from pyvm.payment_errors import describe_payment_failure # noqa: E402
|
||||
from pyvm.protocol import ( # noqa: E402
|
||||
from pyvm.algorithm import generate_encrypt_msg_offline
|
||||
from pyvm.login_profile import midas_login_params
|
||||
from pyvm.mall import MallSession
|
||||
from pyvm.mall import generate_encrypt_msg as mall_generate
|
||||
from pyvm.payment_errors import describe_payment_failure
|
||||
from pyvm.protocol import (
|
||||
GOODS_USER_AGENT,
|
||||
MALL_API_URL,
|
||||
MALL_USER_AGENT,
|
||||
GOODS_USER_AGENT,
|
||||
PAY_APPID,
|
||||
validate_goods_materials,
|
||||
validate_mall_materials,
|
||||
)
|
||||
from pyvm.session import SessionState, load_session
|
||||
|
||||
REPLAY = ROOT / "replay"
|
||||
DEFAULT_APPID = PAY_APPID
|
||||
|
||||
@@ -13,22 +13,24 @@ from .algorithm import (
|
||||
generate_encrypt_msg,
|
||||
generate_encrypt_msg_offline,
|
||||
)
|
||||
from .goods import GoodsSession, generate_encrypt_msg as goods_generate
|
||||
from .mall import MallSession, generate_encrypt_msg as mall_generate
|
||||
from .goods import GoodsSession
|
||||
from .goods import generate_encrypt_msg as goods_generate
|
||||
from .mall import MallSession
|
||||
from .mall import generate_encrypt_msg as mall_generate
|
||||
from .pagedoo_vm import PagedooVM, run_frame
|
||||
from .session import SessionState, load_session
|
||||
|
||||
__all__ = [
|
||||
"GoodsSession",
|
||||
"MallSession",
|
||||
"PagedooVM",
|
||||
"SessionState",
|
||||
"build_plaintext",
|
||||
"decode_d",
|
||||
"generate_encrypt_msg",
|
||||
"generate_encrypt_msg_offline",
|
||||
"GoodsSession",
|
||||
"goods_generate",
|
||||
"MallSession",
|
||||
"mall_generate",
|
||||
"PagedooVM",
|
||||
"run_frame",
|
||||
"SessionState",
|
||||
"load_session",
|
||||
"mall_generate",
|
||||
"run_frame",
|
||||
]
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import math
|
||||
@@ -355,7 +356,7 @@ def js_set(obj, key, val):
|
||||
if isinstance(obj, str):
|
||||
raise TypeError("Cannot assign to read only property")
|
||||
cur = getattr(
|
||||
getattr(__import__("pyvm.algorithm", fromlist=["x"]), "VM"), "_last_u", None
|
||||
__import__("pyvm.algorithm", fromlist=["x"]).VM, "_last_u", None
|
||||
)
|
||||
raise TypeError(
|
||||
f"invalid set target obj={type(obj).__name__} key={key!r} val={type(val).__name__} u={cur}"
|
||||
@@ -418,7 +419,7 @@ class Window:
|
||||
class JSFunction:
|
||||
"""VM 函数(解释器实例工厂返回的 h)。"""
|
||||
|
||||
__slots__ = ("entry", "args", "s", "n", "t", "vm", "name")
|
||||
__slots__ = ("args", "entry", "n", "name", "s", "t", "vm")
|
||||
|
||||
def __init__(self, vm, entry, args, s, n, t):
|
||||
self.vm = vm
|
||||
@@ -455,7 +456,7 @@ def js_call(fn, this, args):
|
||||
return fn.fn(this, *args)
|
||||
if callable(fn):
|
||||
return fn(this, *args)
|
||||
raise TypeError(f"{str(fn)} is not a function u={getattr(VM, '_last_u', None)}")
|
||||
raise TypeError(f"{fn!s} is not a function u={getattr(VM, '_last_u', None)}")
|
||||
|
||||
|
||||
def js_apply(fn, this, args):
|
||||
|
||||
@@ -12,7 +12,6 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .algorithm import generate_encrypt_msg_offline
|
||||
|
||||
@@ -64,7 +63,7 @@ class GoodsSession:
|
||||
raise ValueError("key16/key1 应为 16 字节")
|
||||
|
||||
@classmethod
|
||||
def from_session_state(cls, path: str | Path) -> "GoodsSession":
|
||||
def from_session_state(cls, path: str | Path) -> GoodsSession:
|
||||
"""从 scripts/capture-session.mjs 生成的 session-state.json 加载。"""
|
||||
d = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
return cls(
|
||||
@@ -85,7 +84,7 @@ class GoodsSession:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, d: dict) -> "GoodsSession":
|
||||
def from_json(cls, d: dict) -> GoodsSession:
|
||||
return cls(
|
||||
d["xmidas_ops"],
|
||||
d["key16"],
|
||||
|
||||
@@ -19,7 +19,6 @@ import copy
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .algorithm import UNDEF, JSObject, Window
|
||||
from .pagedoo_vm import PagedooVM
|
||||
@@ -78,7 +77,7 @@ class MallSession:
|
||||
raise ValueError(f"624B 中间态长度 != 624: {len(mid[0])}")
|
||||
|
||||
@classmethod
|
||||
def from_capture_file(cls, frames_jsonl: str | Path) -> "MallSession":
|
||||
def from_capture_file(cls, frames_jsonl: str | Path) -> MallSession:
|
||||
"""从捕获的 frames.jsonl 提取同会话 transform_input + xMidasOps。
|
||||
|
||||
frames.jsonl 由 scripts/capture-mall-session.mjs 实时落盘:
|
||||
@@ -127,7 +126,7 @@ class MallSession:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, d: dict) -> "MallSession":
|
||||
def from_json(cls, d: dict) -> MallSession:
|
||||
return cls(d["transform_input"], d["xmidas_ops"])
|
||||
|
||||
|
||||
|
||||
@@ -13,56 +13,51 @@ JS 语义辅助复用 algorithm.py(_ic/i32/js_add/js_index/JSObject/JSFunction
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
from .algorithm import (
|
||||
JSDate,
|
||||
UNDEF,
|
||||
HostFunction,
|
||||
JSDate,
|
||||
JSFunction,
|
||||
JSObject,
|
||||
HostFunction,
|
||||
_ic,
|
||||
i32,
|
||||
u32,
|
||||
ushr,
|
||||
shl,
|
||||
shr,
|
||||
js_typeof,
|
||||
js_truthy,
|
||||
js_str,
|
||||
js_add,
|
||||
js_num,
|
||||
js_eq,
|
||||
js_streq,
|
||||
js_index,
|
||||
js_set,
|
||||
js_del,
|
||||
js_keys,
|
||||
js_call,
|
||||
js_apply,
|
||||
js_new,
|
||||
h_math_floor,
|
||||
h_math_round,
|
||||
h_math_ceil,
|
||||
h_math_min,
|
||||
h_math_max,
|
||||
h_math_abs,
|
||||
h_math_pow,
|
||||
h_math_sqrt,
|
||||
h_parseint,
|
||||
h_parsefloat,
|
||||
h_isnan,
|
||||
h_encodeuri,
|
||||
h_encodeuricomponent,
|
||||
h_decodeuri,
|
||||
h_decodeuricomponent,
|
||||
h_string_fromcharcode,
|
||||
h_encodeuri,
|
||||
h_encodeuricomponent,
|
||||
h_isnan,
|
||||
h_math_abs,
|
||||
h_math_ceil,
|
||||
h_math_floor,
|
||||
h_math_max,
|
||||
h_math_min,
|
||||
h_math_pow,
|
||||
h_math_round,
|
||||
h_math_sqrt,
|
||||
h_new_date,
|
||||
h_parsefloat,
|
||||
h_parseint,
|
||||
h_string_fromcharcode,
|
||||
i32,
|
||||
js_add,
|
||||
js_apply,
|
||||
js_call,
|
||||
js_del,
|
||||
js_eq,
|
||||
js_index,
|
||||
js_keys,
|
||||
js_new,
|
||||
js_num,
|
||||
js_set,
|
||||
js_str,
|
||||
js_truthy,
|
||||
js_typeof,
|
||||
shl,
|
||||
shr,
|
||||
ushr,
|
||||
)
|
||||
|
||||
__all__ = ["PagedooVM", "run_frame", "REPLAY"]
|
||||
__all__ = ["REPLAY", "PagedooVM", "run_frame"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 辅助
|
||||
|
||||
@@ -10,7 +10,6 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
PAY_APPID = "1450243039"
|
||||
PAY_GOODS_URL = "https://pay.qq.com/midas/minipay_v2/views/cpay/goods.shtml"
|
||||
PAY_SAVE_URL = f"https://api.unipay.qq.com/v1/r/{PAY_APPID}/web_save"
|
||||
|
||||
@@ -68,7 +68,7 @@ class SessionState:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, d: dict) -> "SessionState":
|
||||
def from_json(cls, d: dict) -> SessionState:
|
||||
return cls(
|
||||
xmidas_ops=list(d.get("xmidas_ops", [])),
|
||||
key16=list(d.get("key16", [])),
|
||||
|
||||
@@ -31,10 +31,10 @@ from pyvm.algorithm import (
|
||||
build_plaintext,
|
||||
derive_key1_from_key16,
|
||||
generate_encrypt_msg_offline,
|
||||
) # noqa: E402
|
||||
from pyvm.login_profile import midas_login_params # noqa: E402
|
||||
from pyvm.payment_errors import describe_payment_failure # noqa: E402
|
||||
from pyvm.protocol import ( # noqa: E402
|
||||
)
|
||||
from pyvm.login_profile import midas_login_params
|
||||
from pyvm.payment_errors import describe_payment_failure
|
||||
from pyvm.protocol import (
|
||||
DEFAULT_ORDER_PF,
|
||||
GOODS_USER_AGENT,
|
||||
PAY_APPID,
|
||||
@@ -450,7 +450,6 @@ def cmd_check_only(session_path: Path, out_dir: Path) -> int:
|
||||
from pyvm.order_status import (
|
||||
completion_summary,
|
||||
get_official_orders,
|
||||
is_finished,
|
||||
)
|
||||
|
||||
session = load_json(session_path)
|
||||
@@ -697,7 +696,7 @@ def main() -> int:
|
||||
out_dir, fields, int(fp.get("device_fp_length", 0)), plaintext_length
|
||||
)
|
||||
body = urllib.parse.urlencode(fields)
|
||||
from pyvm.order_status import order_completion_states, get_official_orders
|
||||
from pyvm.order_status import get_official_orders, order_completion_states
|
||||
|
||||
# 尽量在发起支付前建立基线,缩小极快支付造成的检测窗口;失败不阻塞支付,付款码生成后重试。
|
||||
baseline_document = None
|
||||
|
||||
@@ -17,8 +17,8 @@ from pathlib import Path
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import (
|
||||
HTTPRedirectHandler,
|
||||
HTTPCookieProcessor,
|
||||
HTTPRedirectHandler,
|
||||
Request,
|
||||
build_opener,
|
||||
)
|
||||
@@ -43,7 +43,7 @@ USER_AGENT = (
|
||||
class NoRedirect(HTTPRedirectHandler):
|
||||
"""Keep OAuth redirects visible while CookieJar receives Set-Cookie headers."""
|
||||
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: D401
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
return None
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ def g_tk(p_skey: str) -> int:
|
||||
|
||||
|
||||
def parse_poll(body: str) -> tuple[int, str]:
|
||||
match = re.search(r"ptuiCB\((.*)\)", body, re.S)
|
||||
match = re.search(r"ptuiCB\((.*)\)", body, re.DOTALL)
|
||||
if not match:
|
||||
raise ValueError("QQ 二维码轮询响应缺少 ptuiCB")
|
||||
values = re.findall(r"'([^']*)'", match.group(1))
|
||||
|
||||
@@ -20,8 +20,8 @@ from pathlib import Path
|
||||
from urllib.error import HTTPError
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import (
|
||||
HTTPRedirectHandler,
|
||||
HTTPCookieProcessor,
|
||||
HTTPRedirectHandler,
|
||||
Request,
|
||||
build_opener,
|
||||
)
|
||||
@@ -42,7 +42,7 @@ USER_AGENT = (
|
||||
class NoRedirect(HTTPRedirectHandler):
|
||||
"""保留 OAuth 回调的 302 和 Set-Cookie。"""
|
||||
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: D401
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
return None
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ def extract_uuid(page: str) -> str:
|
||||
r"connect/qrcode/([A-Za-z0-9_-]{8,64})",
|
||||
r"uuid=([A-Za-z0-9_-]{8,64})",
|
||||
):
|
||||
match = re.search(pattern, page, re.I)
|
||||
match = re.search(pattern, page, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
raise ValueError("微信授权页未找到二维码 UUID")
|
||||
|
||||
@@ -18,7 +18,7 @@ from curl_cffi import requests
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from pyvm.login_profile import midas_login_params # noqa: E402
|
||||
from pyvm.login_profile import midas_login_params
|
||||
|
||||
YYB_APP_ID = 52575843
|
||||
SOURCE_ID = "24292013"
|
||||
|
||||
@@ -32,7 +32,7 @@ WORKER_KEY = os.environ.get("YYB_WORKER_KEY", "")
|
||||
_jobs: dict[str, dict] = {}
|
||||
_lock = threading.Lock()
|
||||
|
||||
from pyvm.payment_errors import describe_payment_failure # noqa: E402
|
||||
from pyvm.payment_errors import describe_payment_failure
|
||||
|
||||
|
||||
def _payment_failure_message(job: dict, phase: str, fallback: str) -> str:
|
||||
@@ -81,13 +81,13 @@ def _safe_log(job: dict, line: str) -> None:
|
||||
clean = re.sub(r"HOLD_[A-Za-z0-9_-]+", "[订单已隐藏]", clean)
|
||||
clean = re.sub(r"(订单\s*[::]\s*)\S+", r"\1[订单已隐藏]", clean)
|
||||
clean = re.sub(
|
||||
r"(?:token|openid|openkey|cookie)=\S+", "[敏感字段已隐藏]", clean, flags=re.I
|
||||
r"(?:token|openid|openkey|cookie)=\S+", "[敏感字段已隐藏]", clean, flags=re.IGNORECASE
|
||||
)
|
||||
clean = re.sub(
|
||||
r"(?:pay_token|web_token|anti_token|session_id|sessionid)\s*[:= ]\s*\S+",
|
||||
"[敏感字段已隐藏]",
|
||||
clean,
|
||||
flags=re.I,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
timestamp = time.strftime("%H:%M:%S")
|
||||
with _lock:
|
||||
@@ -523,7 +523,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
value = self.headers.get("Authorization", "")
|
||||
return value == f"Bearer {WORKER_KEY}"
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
def do_POST(self) -> None:
|
||||
if not self._authorized():
|
||||
return self._json(401, {"detail": "未授权"})
|
||||
path = urlparse(self.path).path.strip("/").split("/")
|
||||
@@ -687,7 +687,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return self._json(500, {"detail": str(exc)})
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
def do_GET(self) -> None:
|
||||
if urlparse(self.path).path == "/health":
|
||||
return self._json(200, {"status": "ok"})
|
||||
if not self._authorized():
|
||||
|
||||
@@ -8,7 +8,6 @@ import unittest
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
|
||||
_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "jsdom-pay.py"
|
||||
_SPEC = importlib.util.spec_from_file_location("yyb_jsdom_pay", _SCRIPT)
|
||||
assert _SPEC and _SPEC.loader
|
||||
@@ -16,8 +15,8 @@ _MODULE = importlib.util.module_from_spec(_SPEC)
|
||||
sys.modules[_SPEC.name] = _MODULE
|
||||
_SPEC.loader.exec_module(_MODULE)
|
||||
|
||||
from pyvm.payment_errors import describe_payment_failure
|
||||
from pyvm.algorithm import derive_key1_from_key16, generate_encrypt_msg_offline
|
||||
from pyvm.payment_errors import describe_payment_failure
|
||||
from pyvm.protocol import goods_material_diagnostics, validate_goods_materials
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from fastapi import HTTPException
|
||||
|
||||
from web.backend.database import Base
|
||||
from web.backend.models import Account, User
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""充值审计日志的权限、查询和脱敏测试。"""
|
||||
|
||||
import pytest
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -17,7 +17,7 @@ class TestCustomCookieOrder:
|
||||
self.db.add(self.user)
|
||||
self.db.flush()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
accounts = {
|
||||
name: Account(username=name, password="p", email="e", email_password="ep")
|
||||
for name in ("account-a", "account-b", "account-c")
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import pytest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from typing import cast
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from web.backend.database import Base
|
||||
from web.backend.models import Account, LoginTask, ProxyConfig, User
|
||||
from web.backend.services.login_service import (
|
||||
LoginBatchRunner,
|
||||
cleanup_orphan_relogin_tasks,
|
||||
)
|
||||
from web.backend.services.cookie_check_service import check_douyu_cookie
|
||||
from web.backend.routers.cookies import (
|
||||
check_cookie_operations,
|
||||
get_cookie,
|
||||
@@ -21,6 +16,11 @@ from web.backend.routers.cookies import (
|
||||
list_cookies,
|
||||
relogin_invalid_cookie_operations,
|
||||
)
|
||||
from web.backend.services.cookie_check_service import check_douyu_cookie
|
||||
from web.backend.services.login_service import (
|
||||
LoginBatchRunner,
|
||||
cleanup_orphan_relogin_tasks,
|
||||
)
|
||||
|
||||
|
||||
class TestCookieOperation:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""精英手册兑换:csrf 复用、新链路接口与浏览器状态机路由的测试。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from unittest.mock import Mock, call
|
||||
|
||||
import pytest
|
||||
|
||||
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
|
||||
|
||||
@@ -7,13 +7,13 @@ from unittest.mock import ANY, AsyncMock, Mock, patch
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from core.douyu import FishFinRechargeClient, FishFinRechargeConfig
|
||||
from web.backend.database import Base
|
||||
from web.backend.models import Account, AuditLog, DouyuTask, LoginTask, User
|
||||
from web.backend.routers.douyu import get_recharge_channel, supplier_recharge_callback
|
||||
from web.backend.services.audit_service import record_audit
|
||||
from web.backend.services.douyu_runner import DouyuBatchRunner
|
||||
from web.backend.services.douyu_service import ensure_douyu_config
|
||||
from web.backend.services.audit_service import record_audit
|
||||
from core.douyu import FishFinRechargeClient, FishFinRechargeConfig
|
||||
|
||||
|
||||
class TestDouyuGoldRechargeChannel:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
from typing import Any, cast
|
||||
from unittest.mock import Mock
|
||||
|
||||
from core.douyu.login import DouyuLogin
|
||||
from core.douyu.login_api_wgapi import WgapiLoginAPI
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
from unittest.mock import Mock
|
||||
|
||||
from core.douyu.activity_client import DouyuActivityClient
|
||||
from web.backend.models import ProxyConfig
|
||||
from web.backend.services.douyu_runner import DouyuBatchRunner
|
||||
from web.backend.services.douyu_runner_core import (
|
||||
DouyuBatchRunnerCore,
|
||||
DOUYU_PROXY_TASK_TYPES,
|
||||
DouyuBatchRunnerCore,
|
||||
)
|
||||
from web.backend.models import ProxyConfig
|
||||
|
||||
|
||||
def _cfg(**overrides) -> ProxyConfig:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import pytest
|
||||
|
||||
from sqlalchemy import create_engine, event
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import pytest
|
||||
import json
|
||||
import re
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""鱼翅直充供应商 API 客户端测试。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from core.douyu.recharge_api import (
|
||||
FishFinRechargeClient,
|
||||
FishFinRechargeConfig,
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
"""虎牙 App 密码登录及相关组件测试。"""
|
||||
|
||||
import pytest
|
||||
|
||||
import base64
|
||||
import os
|
||||
import struct
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from core.huya import (
|
||||
HuyaAppLoginError,
|
||||
HuyaAppPasswordLogin,
|
||||
HuyaAppQrAuthRequiredError,
|
||||
login_huya_app_password,
|
||||
login_huya_password,
|
||||
)
|
||||
from core.huya.app_login import (
|
||||
DEFAULT_GOLDEN_DEV,
|
||||
@@ -30,17 +24,16 @@ from core.huya.login import HuyaLoginResult
|
||||
from core.huya.nonce_forge import K1_DEFAULT, gen_nonce
|
||||
from core.huya.udb_aes import udb_decrypt, udb_encrypt
|
||||
from core.huya.wup_encoder import build_password_login_wup
|
||||
|
||||
from web.backend.database import Base
|
||||
from web.backend.models import User, HuyaAccount
|
||||
from web.backend.schemas import (
|
||||
HuyaAppPasswordLoginRequest,
|
||||
HuyaPasswordLoginSelectedRequest,
|
||||
)
|
||||
from web.backend.models import HuyaAccount, User
|
||||
from web.backend.routers.huya import (
|
||||
app_password_login_account,
|
||||
app_password_login_selected_accounts,
|
||||
)
|
||||
from web.backend.schemas import (
|
||||
HuyaAppPasswordLoginRequest,
|
||||
HuyaPasswordLoginSelectedRequest,
|
||||
)
|
||||
|
||||
|
||||
class TestHuyaAppLogin:
|
||||
@@ -193,9 +186,8 @@ class TestHuyaAppLogin:
|
||||
with patch(
|
||||
"core.huya.app_login.register_device",
|
||||
side_effect=DfpRegistrationError("注册链 HTTP 500"),
|
||||
):
|
||||
with pytest.raises(HuyaAppLoginError, match="注册失败"):
|
||||
login_cred_with_flow("300023887", "pw")
|
||||
), pytest.raises(HuyaAppLoginError, match="注册失败"):
|
||||
login_cred_with_flow("300023887", "pw")
|
||||
|
||||
def test_router_functions(self):
|
||||
mock_res = HuyaLoginResult(
|
||||
|
||||
@@ -7,9 +7,9 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from utils.logger import (
|
||||
_parse_size,
|
||||
_SensitiveDataFilter,
|
||||
_SizeAndDayRotatingFileHandler,
|
||||
_parse_size,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -68,8 +68,8 @@ class TestMigrationSmoke:
|
||||
|
||||
def test_full_chain_applies_on_fresh_sqlite_and_matches_models(self):
|
||||
"""空库执行整条迁移链,校验表/列/索引与模型元数据对齐。"""
|
||||
from web.backend.database import Base
|
||||
import web.backend.database as database_module
|
||||
from web.backend.database import Base
|
||||
|
||||
original_url = database_module.DATABASE_URL
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
|
||||
+3
-4
@@ -14,7 +14,6 @@ from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
||||
_DEFAULT_RETENTION_DAYS = 14
|
||||
_DEFAULT_ROTATION_SIZE = 50 * 1024 * 1024
|
||||
@@ -36,7 +35,7 @@ def _parse_positive_int(value: str | None, default: int) -> int:
|
||||
|
||||
def _parse_size(value: str | None, default: int = _DEFAULT_ROTATION_SIZE) -> int:
|
||||
"""解析 50M、1GiB 等易读大小;非法值保持安全默认值。"""
|
||||
matched = re.fullmatch(r"\s*(\d+)\s*([kmgt]?i?b?)?\s*", str(value or ""), re.I)
|
||||
matched = re.fullmatch(r"\s*(\d+)\s*([kmgt]?i?b?)?\s*", str(value or ""), re.IGNORECASE)
|
||||
if not matched:
|
||||
return default
|
||||
amount = int(matched.group(1))
|
||||
@@ -113,7 +112,7 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler):
|
||||
self._log_dir / f"{self._filename_prefix}-{day.isoformat()}{self._suffix}"
|
||||
)
|
||||
|
||||
def shouldRollover(self, record: logging.LogRecord) -> bool: # noqa: N802
|
||||
def shouldRollover(self, record: logging.LogRecord) -> bool:
|
||||
if datetime.now().date() != self._active_day:
|
||||
return True
|
||||
if self.stream is None:
|
||||
@@ -122,7 +121,7 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler):
|
||||
message = f"{self.format(record)}\n"
|
||||
return self.stream.tell() + len(message.encode("utf-8")) >= self.max_bytes
|
||||
|
||||
def doRollover(self) -> None: # noqa: N802
|
||||
def doRollover(self) -> None:
|
||||
if self.stream is not None:
|
||||
self.stream.close()
|
||||
self.stream = None
|
||||
|
||||
@@ -10,11 +10,10 @@ from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from loguru import logger
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.types import Text, TypeDecorator
|
||||
from loguru import logger
|
||||
|
||||
|
||||
_PREFIX = "enc:v1:"
|
||||
_FALLBACK_SECRET = "douyu-login-py-dev-storage-key-change-me"
|
||||
|
||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from sqlalchemy import create_engine, event
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
@@ -147,6 +147,7 @@ def _seed():
|
||||
def _encrypt_existing_sensitive_data():
|
||||
"""启动时把历史明文敏感数据迁移为密文。"""
|
||||
from loguru import logger
|
||||
|
||||
from .crypto_storage import encrypt_existing_sensitive_data
|
||||
|
||||
changed = encrypt_existing_sensitive_data(engine)
|
||||
|
||||
+9
-9
@@ -1,15 +1,15 @@
|
||||
"""FastAPI 依赖注入"""
|
||||
|
||||
from typing import Optional
|
||||
from fastapi import Depends, HTTPException, Request, status, WebSocket
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.orm import Session
|
||||
from jose import JWTError
|
||||
|
||||
from .database import get_db, SessionLocal
|
||||
from .security import decode_access_token
|
||||
from fastapi import Depends, HTTPException, Request, WebSocket, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import JWTError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .database import SessionLocal, get_db
|
||||
from .models import User
|
||||
from .permissions import get_user_permissions
|
||||
from .security import decode_access_token
|
||||
|
||||
# auto_error=False: 允许 token 为空(后续从 cookie 读取)
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False)
|
||||
@@ -17,7 +17,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=Fals
|
||||
|
||||
def get_current_user(
|
||||
request: Request,
|
||||
token: Optional[str] = Depends(oauth2_scheme),
|
||||
token: str | None = Depends(oauth2_scheme),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
credentials_exc = HTTPException(
|
||||
@@ -61,7 +61,7 @@ def require_permission(permission: str):
|
||||
return checker
|
||||
|
||||
|
||||
def authenticate_websocket(websocket: WebSocket) -> Optional[User]:
|
||||
def authenticate_websocket(websocket: WebSocket) -> User | None:
|
||||
"""WebSocket 认证:从 cookie 或 query param token 中验证用户身份。
|
||||
|
||||
Returns:
|
||||
|
||||
+15
-13
@@ -1,34 +1,36 @@
|
||||
"""FastAPI 入口"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import uvicorn
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from utils import setup_logger
|
||||
|
||||
from .database import init_db
|
||||
from .routers import (
|
||||
auth,
|
||||
users,
|
||||
accounts,
|
||||
account_check,
|
||||
accounts,
|
||||
audit,
|
||||
auth,
|
||||
cookies,
|
||||
dashboard,
|
||||
douyu,
|
||||
huya,
|
||||
login,
|
||||
proxy,
|
||||
cookies,
|
||||
huya,
|
||||
douyu,
|
||||
users,
|
||||
yyb,
|
||||
audit,
|
||||
)
|
||||
from .schemas import AppInfo
|
||||
from .version import get_app_version
|
||||
from utils import setup_logger
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -45,9 +47,9 @@ async def lifespan(app: FastAPI):
|
||||
from loguru import logger
|
||||
|
||||
from .database import SessionLocal
|
||||
from .services.login_service import cleanup_orphan_relogin_tasks
|
||||
from .services.huya_service import cleanup_orphan_huya_tasks
|
||||
from .services.douyu_service import cleanup_orphan_douyu_tasks
|
||||
from .services.huya_service import cleanup_orphan_huya_tasks
|
||||
from .services.login_service import cleanup_orphan_relogin_tasks
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Alembic 迁移环境。"""
|
||||
|
||||
from logging.config import fileConfig
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sys
|
||||
from logging.config import fileConfig
|
||||
from pathlib import Path
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
@@ -12,8 +12,8 @@ ROOT_DIR = Path(__file__).resolve().parents[3]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
from web.backend.database import Base, DATABASE_URL # noqa: E402
|
||||
from web.backend import models # noqa: F401,E402
|
||||
from web.backend import models # noqa: F401
|
||||
from web.backend.database import DATABASE_URL, Base
|
||||
|
||||
config = context.config
|
||||
config.set_main_option("sqlalchemy.url", DATABASE_URL)
|
||||
|
||||
@@ -5,15 +5,15 @@ Revises:
|
||||
Create Date: 2026-06-23
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260623_0001"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = None
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _has_table(bind, table_name: str) -> bool:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260623_0001
|
||||
Create Date: 2026-06-24
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260624_0002"
|
||||
down_revision: Union[str, None] = "20260623_0001"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260623_0001"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260624_0002
|
||||
Create Date: 2026-06-24
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260624_0003"
|
||||
down_revision: Union[str, None] = "20260624_0002"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260624_0002"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260624_0003
|
||||
Create Date: 2026-07-04
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260704_0004"
|
||||
down_revision: Union[str, None] = "20260624_0003"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260624_0003"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _has_table(bind, table_name: str) -> bool:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260704_0004
|
||||
Create Date: 2026-07-04
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260704_0005"
|
||||
down_revision: Union[str, None] = "20260704_0004"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260704_0004"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _has_table(bind, table_name: str) -> bool:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260704_0005
|
||||
Create Date: 2026-07-05
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260705_0006"
|
||||
down_revision: Union[str, None] = "20260704_0005"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260704_0005"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _has_table(bind, table_name: str) -> bool:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260705_0006
|
||||
Create Date: 2026-07-12
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260712_0007"
|
||||
down_revision: Union[str, None] = "20260705_0006"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260705_0006"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _has_table(bind, table_name: str) -> bool:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260712_0007
|
||||
Create Date: 2026-07-24
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260724_0008"
|
||||
down_revision: Union[str, None] = "20260712_0007"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260712_0007"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _has_table(bind, table_name: str) -> bool:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260724_0008
|
||||
Create Date: 2026-07-25
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260725_0009"
|
||||
down_revision: Union[str, None] = "20260724_0008"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260724_0008"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _has_table(bind, table_name: str) -> bool:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260725_0009
|
||||
Create Date: 2026-07-28
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260728_0010"
|
||||
down_revision: Union[str, None] = "20260725_0009"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260725_0009"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _columns(bind, table_name: str) -> set[str]:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260728_0010
|
||||
Create Date: 2026-07-28
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260728_0011"
|
||||
down_revision: Union[str, None] = "20260728_0010"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260728_0010"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _columns(bind, table_name: str) -> set[str]:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260728_0011
|
||||
Create Date: 2026-07-28
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260728_0012"
|
||||
down_revision: Union[str, None] = "20260728_0011"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260728_0011"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260728_0012
|
||||
Create Date: 2026-07-28
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260728_0013"
|
||||
down_revision: Union[str, None] = "20260728_0012"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260728_0012"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _columns(bind, table_name: str) -> set[str]:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260728_0013
|
||||
Create Date: 2026-07-28
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260728_0014"
|
||||
down_revision: Union[str, None] = "20260728_0013"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260728_0013"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260728_0014
|
||||
Create Date: 2026-08-05
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260805_0015"
|
||||
down_revision: Union[str, None] = "20260728_0014"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260728_0014"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
INDEXES = [
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260805_0015
|
||||
Create Date: 2026-08-06
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260806_0016"
|
||||
down_revision: Union[str, None] = "20260805_0015"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260805_0015"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _columns(bind, table_name: str) -> set[str]:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260806_0016
|
||||
Create Date: 2026-08-07
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260807_0017"
|
||||
down_revision: Union[str, None] = "20260806_0016"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260806_0016"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260807_0017
|
||||
Create Date: 2026-08-07
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260807_0018"
|
||||
down_revision: Union[str, None] = "20260807_0017"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260807_0017"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260807_0018
|
||||
Create Date: 2026-08-08
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260808_0019"
|
||||
down_revision: Union[str, None] = "20260807_0018"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260807_0018"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"""增加应用宝和平精英充值任务"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260812_0020"
|
||||
down_revision: Union[str, None] = "20260808_0019"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260808_0019"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
"""扩展应用宝二维码密文字段容量"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "20260812_0021"
|
||||
down_revision: Union[str, None] = "20260812_0020"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260812_0020"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _is_mysql() -> bool:
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
"""应用宝支付元数据字段:金额与支付时间线"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260812_0022"
|
||||
down_revision: Union[str, None] = "20260812_0021"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260812_0021"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
"""支持用户软删除并保留历史任务归属"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260813_0023"
|
||||
down_revision: Union[str, None] = "20260812_0022"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260812_0022"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _columns(bind, table_name: str) -> set[str]:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260813_0023
|
||||
Create Date: 2026-08-13
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260813_0024"
|
||||
down_revision: Union[str, None] = "20260813_0023"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260813_0023"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _columns(bind, table_name: str) -> set[str]:
|
||||
|
||||
@@ -5,16 +5,15 @@ Revises: 20260813_0024
|
||||
Create Date: 2026-08-13
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260813_0025"
|
||||
down_revision: Union[str, None] = "20260813_0024"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "20260813_0024"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user