执行 Ruff 安全自动修复

This commit is contained in:
yml2213
2026-08-31 10:28:14 +08:00
parent 2a1d27f953
commit b58c6b4357
145 changed files with 940 additions and 993 deletions
+10 -10
View File
@@ -1,11 +1,11 @@
"""斗鱼登录模块""" """斗鱼登录模块"""
from .login import DouyuLogin, CredentialError
from .login_api import LoginAPIStrategy
from .login_api_wgapi import WgapiLoginAPI
from .login_api_iframe import IframeLoginAPI
from .email_verifier import EmailVerifier
from .activity_client import DouyuActivityClient, DouyuActivityError from .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 ( from .recharge_api import (
FishFinRechargeClient, FishFinRechargeClient,
FishFinRechargeConfig, FishFinRechargeConfig,
@@ -14,16 +14,16 @@ from .recharge_api import (
) )
__all__ = [ __all__ = [
"DouyuLogin",
"CredentialError", "CredentialError",
"LoginAPIStrategy",
"WgapiLoginAPI",
"IframeLoginAPI",
"EmailVerifier",
"DouyuActivityClient", "DouyuActivityClient",
"DouyuActivityError", "DouyuActivityError",
"DouyuLogin",
"EmailVerifier",
"FishFinRechargeClient", "FishFinRechargeClient",
"FishFinRechargeConfig", "FishFinRechargeConfig",
"FishFinRechargeConfigError", "FishFinRechargeConfigError",
"FishFinRechargeError", "FishFinRechargeError",
"IframeLoginAPI",
"LoginAPIStrategy",
"WgapiLoginAPI",
] ]
+1 -3
View File
@@ -18,7 +18,6 @@ from .cookie_utils import (
normalize_douyu_cookie, normalize_douyu_cookie,
) )
PC_UA = ( PC_UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) " "AppleWebKit/537.36 (KHTML, like Gecko) "
@@ -1446,8 +1445,7 @@ class DouyuActivityClient:
if not match: if not match:
raise DouyuActivityError(f"响应中找不到 var {varname}=: {text[:200]}") raise DouyuActivityError(f"响应中找不到 var {varname}=: {text[:200]}")
chunk = text[match.end() :].strip() chunk = text[match.end() :].strip()
if chunk.endswith(";"): chunk = chunk.removesuffix(";")
chunk = chunk[:-1]
try: try:
return json.loads(chunk) return json.loads(chunk)
except json.JSONDecodeError as exc: except json.JSONDecodeError as exc:
+2 -4
View File
@@ -2,11 +2,9 @@
import base64 import base64
import hashlib 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中提取) # 斗鱼RSA公钥(从JS中提取)
DOUYU_RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- DOUYU_RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
+11 -14
View File
@@ -6,11 +6,9 @@ import re
import threading import threading
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional
from loguru import logger
import requests import requests
from loguru import logger
# Roundcube Webmail 地址。可通过环境变量覆盖。空值时回退到默认。 # Roundcube Webmail 地址。可通过环境变量覆盖。空值时回退到默认。
ROUNDCUBE_URL = os.getenv("MAIL_ROUNDCUBE_URL") or "http://111.229.206.54:8000/" ROUNDCUBE_URL = os.getenv("MAIL_ROUNDCUBE_URL") or "http://111.229.206.54:8000/"
@@ -40,7 +38,7 @@ class EmailVerifier:
max_messages: int = 20, max_messages: int = 20,
use_ssl: bool = False, use_ssl: bool = False,
roundcube_url: str = "", roundcube_url: str = "",
backup_passwords: Optional[list[str]] = None, backup_passwords: list[str] | None = None,
): ):
# 兼容旧参数名 # 兼容旧参数名
self.imap_server = imap_server self.imap_server = imap_server
@@ -54,14 +52,13 @@ class EmailVerifier:
) )
# Roundcube 会话(懒初始化) # Roundcube 会话(懒初始化)
self._rc_session: Optional[requests.Session] = None self._rc_session: requests.Session | None = None
self._rc_token: Optional[str] = None self._rc_token: str | None = None
self._rc_logged_in = False self._rc_logged_in = False
self._rc_login_error = "" self._rc_login_error = ""
def connect(self) -> None: def connect(self) -> None:
"""兼容接口""" """兼容接口"""
pass
def disconnect(self) -> None: def disconnect(self) -> None:
"""关闭 Roundcube 会话""" """关闭 Roundcube 会话"""
@@ -239,7 +236,7 @@ class EmailVerifier:
# ── Roundcube 读取邮件内容 ───────────────────────────── # ── Roundcube 读取邮件内容 ─────────────────────────────
def _rc_fetch_email_body(self, uid: int) -> Optional[str]: def _rc_fetch_email_body(self, uid: int) -> str | None:
"""通过 Roundcube 读取指定 UID 邮件的正文""" """通过 Roundcube 读取指定 UID 邮件的正文"""
if not self._ensure_roundcube_session(): if not self._ensure_roundcube_session():
if self._rc_login_error: if self._rc_login_error:
@@ -262,7 +259,7 @@ class EmailVerifier:
# ── Roundcube 日期解析 ───────────────────────────────── # ── Roundcube 日期解析 ─────────────────────────────────
@staticmethod @staticmethod
def _parse_rc_date(date_str: str) -> Optional[datetime]: def _parse_rc_date(date_str: str) -> datetime | None:
""" """
解析 Roundcube 返回的日期字符串为 datetime。 解析 Roundcube 返回的日期字符串为 datetime。
@@ -363,9 +360,9 @@ class EmailVerifier:
self, self,
max_wait: int = 60, max_wait: int = 60,
interval: int = 3, interval: int = 3,
after_timestamp: Optional[float] = None, after_timestamp: float | None = None,
allow_old_seconds: int = 15, allow_old_seconds: int = 15,
stop_event: Optional[threading.Event] = None, stop_event: threading.Event | None = None,
) -> str: ) -> str:
""" """
轮询获取斗鱼验证码。 轮询获取斗鱼验证码。
@@ -417,9 +414,9 @@ class EmailVerifier:
def _fetch_code_via_roundcube( def _fetch_code_via_roundcube(
self, self,
after_timestamp: Optional[float] = None, after_timestamp: float | None = None,
allow_old_seconds: int = 15, allow_old_seconds: int = 15,
) -> Optional[str]: ) -> str | None:
"""通过 Roundcube API 获取最新斗鱼验证码""" """通过 Roundcube API 获取最新斗鱼验证码"""
messages = self._rc_fetch_mail_list() 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位验证码""" """从文本中提取6位验证码"""
# 清理HTML标签和实体 # 清理HTML标签和实体
text = html.unescape(text) text = html.unescape(text)
+26 -24
View File
@@ -4,28 +4,30 @@ import json
import re import re
import threading import threading
import time import time
import requests from collections.abc import Mapping
from typing import Mapping, Optional, Protocol, Tuple from typing import Protocol
from urllib.parse import urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit
import requests
from loguru import logger 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 .cookie_enricher import CookieEnricher
from .email_verifier import EmailLoginError, EmailVerifier from .email_verifier import EmailLoginError, EmailVerifier
from .login_api import LoginAPIStrategy from .login_api import LoginAPIStrategy
from .login_api_wgapi import WgapiLoginAPI from .login_api_wgapi import WgapiLoginAPI
from .proxy_fetcher import ProxyFetcher 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个线程做极验验证 ── # ── 全局极验并发限制:同一时刻最多2个线程做极验验证 ──
_geetest_semaphore = threading.Semaphore(2) _geetest_semaphore = threading.Semaphore(2)
@@ -95,14 +97,14 @@ class DouyuLogin:
def __init__( def __init__(
self, self,
account: AccountLike, account: AccountLike,
proxy: Optional[str | Mapping[str, str]] = None, proxy: str | Mapping[str, str] | None = None,
proxy_api_url: Optional[str] = None, proxy_api_url: str | None = None,
timeout: tuple[float, float] = REQUEST_TIMEOUT, timeout: tuple[float, float] = REQUEST_TIMEOUT,
max_login_retries: int = 0, max_login_retries: int = 0,
max_total_time: float = 0, max_total_time: float = 0,
proxy_fetcher: Optional[ProxyFetcher] = None, proxy_fetcher: ProxyFetcher | None = None,
stop_event: Optional[threading.Event] = None, stop_event: threading.Event | None = None,
api_strategy: Optional[LoginAPIStrategy] = None, api_strategy: LoginAPIStrategy | None = None,
): ):
self.account = account self.account = account
self.proxy = proxy self.proxy = proxy
@@ -117,7 +119,7 @@ class DouyuLogin:
ProxyFetcher(api_url=proxy_api_url) if proxy_api_url else None 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._cookie_enrich_error = ""
self._static_retry_count = 0 self._static_retry_count = 0
self._setup_session() self._setup_session()
@@ -565,7 +567,7 @@ class DouyuLogin:
) )
return True 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( def _solve_geetest(
self, gt: str, challenge: str, deadline: float = 0 self, gt: str, challenge: str, deadline: float = 0
) -> Tuple[str, str]: ) -> tuple[str, str]:
""" """
解决极验 fullpage 验证(最多3次尝试,失败直接抛异常回到login换新代理) 解决极验 fullpage 验证(最多3次尝试,失败直接抛异常回到login换新代理)
@@ -635,7 +637,7 @@ class DouyuLogin:
def _solve_geetest_inner( def _solve_geetest_inner(
self, gt: str, challenge: str, deadline: float = 0 self, gt: str, challenge: str, deadline: float = 0
) -> Tuple[str, str]: ) -> tuple[str, str]:
"""极验验证内部实现:最多3次尝试,失败直接抛异常。""" """极验验证内部实现:最多3次尝试,失败直接抛异常。"""
_MAX_ATTEMPTS = 3 _MAX_ATTEMPTS = 3
@@ -652,7 +654,7 @@ class DouyuLogin:
# 超时兜底 # 超时兜底
if deadline and time.monotonic() > deadline: if deadline and time.monotonic() > deadline:
raise ValueError(f"极验验证超时(登录整体时间耗尽)") raise ValueError("极验验证超时(登录整体时间耗尽)")
try: try:
logger.info(f"极验验证尝试 {attempt}/{_MAX_ATTEMPTS}") logger.info(f"极验验证尝试 {attempt}/{_MAX_ATTEMPTS}")
@@ -825,7 +827,7 @@ class DouyuLogin:
logger.info("验证邮件已发送") 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( verifier = EmailVerifier(
imap_server=self.account.email_imap_server, imap_server=self.account.email_imap_server,
+1 -2
View File
@@ -1,7 +1,6 @@
"""登录接口策略基类""" """登录接口策略基类"""
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Tuple
class LoginAPIStrategy(ABC): 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: Args:
+1
View File
@@ -10,6 +10,7 @@
import threading import threading
import time import time
from loguru import logger from loguru import logger
from .proxy_resolver import ProxyResolver from .proxy_resolver import ProxyResolver
+1 -2
View File
@@ -2,10 +2,9 @@
import json import json
import re 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 格式与旧版纯文本格式。 解析代理 API 响应,支持 JSON 格式与旧版纯文本格式。
+2 -2
View File
@@ -13,7 +13,7 @@ from .xiequ import XiequAdapter
from .xkdaili import XkdailiAdapter from .xkdaili import XkdailiAdapter
# ── 平台注册表 ── # ── 平台注册表 ──
_PLATFORM_REGISTRY: dict[str, Type[BaseWhitelistAdapter]] = { _PLATFORM_REGISTRY: dict[str, type[BaseWhitelistAdapter]] = {
"xiequ": XiequAdapter, "xiequ": XiequAdapter,
"xkdaili": XkdailiAdapter, "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) cls = _PLATFORM_REGISTRY.get(platform)
if not cls: if not cls:
+3 -4
View File
@@ -8,7 +8,6 @@ import re
import threading import threading
import time import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Optional
import requests import requests
from loguru import logger from loguru import logger
@@ -144,12 +143,12 @@ class BaseWhitelistAdapter(ABC):
return False, msg return False, msg
@staticmethod @staticmethod
def get_local_exit_ip() -> Optional[str]: def get_local_exit_ip() -> str | None:
"""获取本机当前公网出口IP(不走代理)。""" """获取本机当前公网出口IP(不走代理)。"""
return _get_local_exit_ip() return _get_local_exit_ip()
def _get_local_exit_ip() -> Optional[str]: def _get_local_exit_ip() -> str | None:
"""获取本机当前公网出口IP(不走代理)。""" """获取本机当前公网出口IP(不走代理)。"""
targets = [ targets = [
"https://myip.ipip.net", "https://myip.ipip.net",
@@ -171,7 +170,7 @@ def _get_local_exit_ip() -> Optional[str]:
return None return None
def get_exit_ip_via_proxy(proxy: str) -> Optional[str]: def get_exit_ip_via_proxy(proxy: str) -> str | None:
"""通过代理获取出口IP。""" """通过代理获取出口IP。"""
targets = [ targets = [
"https://qifu-api.baidubce.com/ip/local/geo/v1/district", "https://qifu-api.baidubce.com/ip/local/geo/v1/district",
+10 -9
View File
@@ -2,7 +2,8 @@
import threading import threading
import time import time
from typing import Callable, Optional, Protocol from collections.abc import Callable
from typing import Protocol
import requests import requests
from loguru import logger from loguru import logger
@@ -19,7 +20,7 @@ class WhitelistSyncer(Protocol):
def sync_ip(self, ip: str) -> tuple[bool, str]: ... 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: class ProxyResolver:
@@ -28,11 +29,11 @@ class ProxyResolver:
def __init__( def __init__(
self, self,
api_url: str, api_url: str,
whitelist_syncer: Optional[WhitelistSyncer] = None, whitelist_syncer: WhitelistSyncer | None = None,
log_func: Optional[LogFunc] = None, log_func: LogFunc | None = None,
sync_local_exit_ip: bool = False, sync_local_exit_ip: bool = False,
sync_whitelist_once: bool = True, sync_whitelist_once: bool = True,
stop_event: Optional[threading.Event] = None, stop_event: threading.Event | None = None,
): ):
self.api_url = api_url self.api_url = api_url
self.whitelist_syncer = whitelist_syncer self.whitelist_syncer = whitelist_syncer
@@ -40,7 +41,7 @@ class ProxyResolver:
self.sync_local_exit_ip = sync_local_exit_ip self.sync_local_exit_ip = sync_local_exit_ip
self.sync_whitelist_once = sync_whitelist_once self.sync_whitelist_once = sync_whitelist_once
self.stop_event = stop_event self.stop_event = stop_event
self._last_synced_ip: Optional[str] = None self._last_synced_ip: str | None = None
self._has_synced_whitelist = False self._has_synced_whitelist = False
def _is_stopped(self) -> bool: def _is_stopped(self) -> bool:
@@ -95,7 +96,7 @@ class ProxyResolver:
self, self,
max_attempts: int = 4, max_attempts: int = 4,
return_all: bool = False, 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_platform: str = "xiequ",
whitelist_credentials: dict | None = None, whitelist_credentials: dict | None = None,
max_attempts: int = 4, max_attempts: int = 4,
log_func: Optional[LogFunc] = None, log_func: LogFunc | None = None,
) -> tuple[Optional[str], str]: ) -> tuple[str | None, str]:
"""从代理 API 获取可用代理,自动处理白名单同步。""" """从代理 API 获取可用代理,自动处理白名单同步。"""
syncer = ( syncer = (
DouyuWhitelistSyncer( DouyuWhitelistSyncer(
+1 -2
View File
@@ -1,7 +1,6 @@
"""代理可用性验证。""" """代理可用性验证。"""
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Optional
import requests import requests
from loguru import logger from loguru import logger
@@ -45,7 +44,7 @@ def verify_proxies_concurrent(
timeout: tuple = (5, 8), timeout: tuple = (5, 8),
max_workers: int = 5, max_workers: int = 5,
return_all: bool = False, return_all: bool = False,
) -> tuple[Optional[str | list[str]], str]: ) -> tuple[str | list[str] | None, str]:
""" """
并发验证多个代理 URL。 并发验证多个代理 URL。
+3 -4
View File
@@ -1,6 +1,5 @@
"""代理模块使用的白名单适配器。""" """代理模块使用的白名单适配器。"""
from typing import Optional
from .proxy_platforms import create_adapter from .proxy_platforms import create_adapter
from .proxy_platforms.base import BaseWhitelistAdapter, _get_local_exit_ip from .proxy_platforms.base import BaseWhitelistAdapter, _get_local_exit_ip
@@ -17,7 +16,7 @@ class DouyuWhitelistSyncer:
def __init__( def __init__(
self, self,
platform: str = "xiequ", platform: str = "xiequ",
credentials: Optional[dict] = None, credentials: dict | None = None,
uid: str = "", uid: str = "",
ukey: str = "", ukey: str = "",
): ):
@@ -28,7 +27,7 @@ class DouyuWhitelistSyncer:
self.platform = platform self.platform = platform
self.credentials = credentials or {} self.credentials = credentials or {}
self._adapter: Optional[BaseWhitelistAdapter] = None self._adapter: BaseWhitelistAdapter | None = None
if self.credentials: if self.credentials:
self._adapter = create_adapter(platform, self.credentials) self._adapter = create_adapter(platform, self.credentials)
@@ -38,5 +37,5 @@ class DouyuWhitelistSyncer:
return False, "未配置白名单凭据" return False, "未配置白名单凭据"
return self._adapter.sync_ip(ip) 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() return _get_local_exit_ip()
+7 -6
View File
@@ -6,9 +6,10 @@ import hashlib
import json import json
import os import os
import time import time
from collections.abc import Callable, Mapping
from dataclasses import dataclass from dataclasses import dataclass
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from typing import Any, Callable, Mapping from typing import Any
import requests import requests
@@ -34,7 +35,7 @@ class FishFinRechargeConfig:
debug: bool = False debug: bool = False
@classmethod @classmethod
def from_env(cls) -> "FishFinRechargeConfig": def from_env(cls) -> FishFinRechargeConfig:
"""从环境变量读取配置,不在代码或数据库中保存商户密钥。""" """从环境变量读取配置,不在代码或数据库中保存商户密钥。"""
timeout = float(os.getenv("FISH_FIN_RECHARGE_TIMEOUT", "20")) timeout = float(os.getenv("FISH_FIN_RECHARGE_TIMEOUT", "20"))
return cls( return cls(
@@ -189,7 +190,7 @@ class FishFinRechargeClient:
"sign_params": sign_params, "sign_params": sign_params,
# 不把可重放签名原文或 AppSecret 写入日志,只记录待签名串摘要。 # 不把可重放签名原文或 AppSecret 写入日志,只记录待签名串摘要。
"sign_source_digest": hashlib.sha256( "sign_source_digest": hashlib.sha256(
f"{sign_query}{method.upper()}".encode("utf-8") f"{sign_query}{method.upper()}".encode()
).hexdigest()[:12], ).hexdigest()[:12],
} }
) )
@@ -240,7 +241,7 @@ class FishFinRechargeClient:
return payload return payload
@staticmethod @staticmethod
def _amount(value: Decimal | int | float | str) -> str: def _amount(value: Decimal | float | str) -> str:
"""规范化金额,避免浮点数表达式进入签名或订单请求。""" """规范化金额,避免浮点数表达式进入签名或订单请求。"""
try: try:
price = Decimal(str(value)) price = Decimal(str(value))
@@ -251,7 +252,7 @@ class FishFinRechargeClient:
return format(price.normalize(), "f") return format(price.normalize(), "f")
@classmethod @classmethod
def _json_amount(cls, value: Decimal | int | float | str) -> int | float: def _json_amount(cls, value: Decimal | float | str) -> int | float:
"""按文档以 JSON 数字发送金额,整数不附带无意义的小数位。""" """按文档以 JSON 数字发送金额,整数不附带无意义的小数位。"""
amount_text = cls._amount(value) amount_text = cls._amount(value)
return int(amount_text) if "." not in amount_text else float(amount_text) return int(amount_text) if "." not in amount_text else float(amount_text)
@@ -267,7 +268,7 @@ class FishFinRechargeClient:
self, self,
*, *,
buy_num: int, buy_num: int,
pay_amount: Decimal | int | float | str, pay_amount: Decimal | float | str,
out_order_id: str, out_order_id: str,
product_id: str, product_id: str,
recharge_arg: list[dict[str, Any]], recharge_arg: list[dict[str, Any]],
+3 -2
View File
@@ -1,8 +1,9 @@
import random
import hashlib import hashlib
import random
from typing import Any from typing import Any
from Crypto.Cipher import AES, PKCS1_v1_5
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5, AES
# 随机产生4个字符组成的字符串 # 随机产生4个字符组成的字符串
+2 -2
View File
@@ -1,8 +1,8 @@
import requests
import cv2 import cv2
import numpy as np import numpy as np
from PIL import Image import requests
from loguru import logger from loguru import logger
from PIL import Image
REQUEST_TIMEOUT = (3.05, 12) REQUEST_TIMEOUT = (3.05, 12)
+13 -12
View File
@@ -1,8 +1,9 @@
import time
import requests
import json import json
import re import re
from typing import Mapping, Optional, Tuple import time
from collections.abc import Mapping
import requests
from loguru import logger from loguru import logger
REQUEST_TIMEOUT = (10, 30) REQUEST_TIMEOUT = (10, 30)
@@ -12,9 +13,9 @@ PASSPORT_REFERER = "https://passport.douyu.com/"
def _get( def _get(
url: str, url: str,
*, *,
params: Optional[dict] = None, params: dict | None = None,
headers: Optional[dict] = None, headers: dict | None = None,
proxies: Optional[Mapping[str, str]] = None, proxies: Mapping[str, str] | None = None,
) -> requests.Response: ) -> requests.Response:
"""发送极验 GET 请求,确保使用同一个代理出口。""" """发送极验 GET 请求,确保使用同一个代理出口。"""
return requests.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 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 = { headers = {
"accept": "application/json, text/javascript, */*; q=0.01", "accept": "application/json, text/javascript, */*; q=0.01",
"accept-language": "zh-CN,zh;q=0.9", "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"] return data["gt"], data["challenge"]
def get_challenge_gt() -> Tuple[str, str]: def get_challenge_gt() -> tuple[str, str]:
headers = { 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", "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", "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 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 = { headers = {
"accept": "*/*", "accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9", "accept-language": "zh-CN,zh;q=0.9",
@@ -149,8 +150,8 @@ def get_c_s(
gt: str, gt: str,
challenge: str, challenge: str,
w: str, w: str,
proxies: Optional[Mapping[str, str]] = None, proxies: Mapping[str, str] | None = None,
) -> Tuple[list[int], str]: ) -> tuple[list[int], str]:
headers = { headers = {
"accept": "*/*", "accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9", "accept-language": "zh-CN,zh;q=0.9",
@@ -184,7 +185,7 @@ def req_fullpage_validate(
gt: str, gt: str,
challenge: str, challenge: str,
w: str, w: str,
proxies: Optional[Mapping[str, str]] = None, proxies: Mapping[str, str] | None = None,
) -> dict: ) -> dict:
"""HAR 中的 fullpage 最终校验,成功后直接返回 validate。""" """HAR 中的 fullpage 最终校验,成功后直接返回 validate。"""
headers = { headers = {
+2 -3
View File
@@ -1,9 +1,8 @@
import time
import random import random
from typing import Optional import time
def generate_fake_performance_timing(base_time: Optional[int] = None) -> dict[str, int]: def generate_fake_performance_timing(base_time: int | None = None) -> dict[str, int]:
""" """
生成伪造的浏览器性能时间戳数据 生成伪造的浏览器性能时间戳数据
+3 -3
View File
@@ -1,6 +1,6 @@
import random
import math 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( def process_mouse_trajectory(
events: list[Any], max_records: Optional[int] = None events: list[Any], max_records: int | None = None
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
处理鼠标/触摸轨迹数据,将绝对坐标转换为相对坐标和时间差 处理鼠标/触摸轨迹数据,将绝对坐标转换为相对坐标和时间差
+16 -14
View File
@@ -1,35 +1,37 @@
import time
import random
import json import json
import random
import time
from loguru import logger 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 ( from core.geetest.common.crypto import (
four_random_chart,
RSA_jiami_r,
AES_O, AES_O,
geetest_base64_encode, RSA_jiami_r,
encrypt_string, encrypt_string,
four_random_chart,
geetest_base64_encode,
simple_md5, simple_md5,
) )
from core.geetest.common.imaging import download_picture from core.geetest.common.imaging import download_picture
from core.geetest.common.network import ( from core.geetest.common.network import (
get_c_s,
get_challenge_gt, get_challenge_gt,
get_js_address, get_js_address,
get_c_s,
req_slide,
get_picture, get_picture,
req_end, req_end,
req_slide,
) )
from core.geetest.common.performance import ( from core.geetest.common.performance import (
generate_fake_performance_timing, generate_fake_performance_timing,
get_slide_track, get_slide_track,
) )
from core.geetest.common.trajectory import (
H,
TrajectoryEncoder,
compress_trajectory,
generate_realistic_trajectory,
process_mouse_trajectory,
)
def _generate_seed() -> str: def _generate_seed() -> str:
+7 -7
View File
@@ -33,23 +33,23 @@ if TYPE_CHECKING:
) )
__all__ = [ __all__ = [
"HuyaHttpClient",
"HuyaWssClient",
"GetUserScoreReq", "GetUserScoreReq",
"GetUserScoreResp", "GetUserScoreResp",
"HuyaAppLoginError",
"HuyaAppPasswordLogin",
"HuyaAppQrAuthRequiredError",
"HuyaCredentialError", "HuyaCredentialError",
"HuyaHttpClient",
"HuyaLoginError", "HuyaLoginError",
"HuyaLoginResult", "HuyaLoginResult",
"HuyaPasswordLogin", "HuyaPasswordLogin",
"HuyaAppLoginError",
"HuyaAppQrAuthRequiredError",
"HuyaAppPasswordLogin",
"HuyaSmsCodeResult", "HuyaSmsCodeResult",
"HuyaSmsLogin", "HuyaSmsLogin",
"HuyaVerificationError", "HuyaVerificationError",
"HuyaVerificationSolver", "HuyaVerificationSolver",
"login_huya_password", "HuyaWssClient",
"login_huya_app_password", "login_huya_app_password",
"login_huya_password",
"login_huya_sms", "login_huya_sms",
"send_huya_sms_code", "send_huya_sms_code",
"solve_huya_verification", "solve_huya_verification",
@@ -98,8 +98,8 @@ def __getattr__(name: str):
}: }:
from .app_login import ( from .app_login import (
HuyaAppLoginError, HuyaAppLoginError,
HuyaAppQrAuthRequiredError,
HuyaAppPasswordLogin, HuyaAppPasswordLogin,
HuyaAppQrAuthRequiredError,
login_huya_app_password, login_huya_app_password,
) )
+1 -2
View File
@@ -35,13 +35,12 @@ import os
import sys import sys
import time import time
from .app_login import HuyaAppPasswordLogin
from .device_profile import ( from .device_profile import (
HDID32, # 登录帧 t1.t0 32hex 常量 (R15 公式结果, k1 来源见 R39; device_profile.py)
_load_db, _load_db,
_save_db, _save_db,
get_profile, # 幂等画像: 同账号永远复用同一套 (data/huya_device_profiles.json) get_profile, # 幂等画像: 同账号永远复用同一套 (data/huya_device_profiles.json)
) )
from .app_login import HuyaAppPasswordLogin
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+2 -2
View File
@@ -130,7 +130,7 @@ def wup_password_login_raw(
固定 action/device_id。风控重试调用方应显式复用同一注册结果。 固定 action/device_id。风控重试调用方应显式复用同一注册结果。
注册链失败抛 ``HuyaAppLoginError``,绝不静默回退旧固定值。 注册链失败抛 ``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) dev = dict(device_info) if device_info is not None else get_profile(account)
mj, ua, _old_sd = _golden_session_assets() mj, ua, _old_sd = _golden_session_assets()
if not safedeviceid: if not safedeviceid:
@@ -304,7 +304,7 @@ def login_cred_with_flow(
logger.info(f"[huya-app] 第 {rnd + 1} 轮触发安全验证: {kind}") logger.info(f"[huya-app] 第 {rnd + 1} 轮触发安全验证: {kind}")
if "qr_auth" in risk_url: if "qr_auth" in risk_url:
raise HuyaAppQrAuthRequiredError( raise HuyaAppQrAuthRequiredError(
f"该账号 App 渠道要求扫码验证(qr_auth),请先在手机虎牙 App 上正常登录一次建立设备信任。" "该账号 App 渠道要求扫码验证(qr_auth),请先在手机虎牙 App 上正常登录一次建立设备信任。"
) )
solve_safe_auth(risk_url, proxies=proxies, device_info=dev) solve_safe_auth(risk_url, proxies=proxies, device_info=dev)
logger.info("[huya-app] safe_auth 滑块过验成功,重发 WUP 登录...") logger.info("[huya-app] safe_auth 滑块过验成功,重发 WUP 登录...")
-1
View File
@@ -5,7 +5,6 @@
from __future__ import annotations from __future__ import annotations
import base64
import os import os
import struct import struct
-1
View File
@@ -30,7 +30,6 @@ from .login import (
generate_request_id, generate_request_id,
) )
CHANGE_PASSWORD_VERSION = "2.5" CHANGE_PASSWORD_VERSION = "2.5"
CHANGE_PASSWORD_CHECK_URI = "60011" CHANGE_PASSWORD_CHECK_URI = "60011"
CHANGE_PASSWORD_SEND_SMS_URI = "60003" CHANGE_PASSWORD_SEND_SMS_URI = "60003"
-1
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Iterable, Mapping from collections.abc import Iterable, Mapping
import requests
from requests.cookies import RequestsCookieJar from requests.cookies import RequestsCookieJar
+5 -5
View File
@@ -116,7 +116,7 @@ class Envelope:
self._parse() self._parse()
@classmethod @classmethod
def load(cls, path: str | Path | None = None) -> "Envelope": def load(cls, path: str | Path | None = None) -> Envelope:
"""加载信封模板,支持从文件加载或使用内嵌金样本。""" """加载信封模板,支持从文件加载或使用内嵌金样本。"""
if path: if path:
p = Path(path) p = Path(path)
@@ -136,7 +136,7 @@ class Envelope:
return cls(base64.b64decode(DEFAULT_QURL_B64)) return cls(base64.b64decode(DEFAULT_QURL_B64))
@classmethod @classmethod
def _load_from_path(cls, p: Path) -> "Envelope": def _load_from_path(cls, p: Path) -> Envelope:
if p.suffix == ".json": if p.suffix == ".json":
j = json.loads(p.read_text("utf-8")) j = json.loads(p.read_text("utf-8"))
q = next( q = next(
@@ -236,7 +236,7 @@ class Envelope:
assert self.uid_off is not None assert self.uid_off is not None
return struct.unpack_from(">Q", self.raw, self.uid_off)[0] 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 assert self.uid_off is not None
struct.pack_into(">Q", self.raw, self.uid_off, uid) struct.pack_into(">Q", self.raw, self.uid_off, uid)
return self return self
@@ -246,7 +246,7 @@ class Envelope:
assert self.cert_off is not None assert self.cert_off is not None
return bytes(self.raw[self.cert_off : self.cert_off + self.cert_len]) 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 assert self.cert_off is not None
b64 = base64.b64encode(cert) b64 = base64.b64encode(cert)
if len(b64) != self.cert_len: if len(b64) != self.cert_len:
@@ -256,7 +256,7 @@ class Envelope:
self.raw[self.cert_off : self.cert_off + self.cert_len] = b64 self.raw[self.cert_off : self.cert_off + self.cert_len] = b64
return self return self
def patch_session(self, session: int) -> "Envelope": def patch_session(self, session: int) -> Envelope:
if self.tag4_span: if self.tag4_span:
struct.pack_into(">I", self.raw, self.tag4_span[0], session & 0xFFFFFFFF) struct.pack_into(">I", self.raw, self.tag4_span[0], session & 0xFFFFFFFF)
d = self.raw d = self.raw
+3 -8
View File
@@ -3,6 +3,7 @@ TAF/WUP 帧解码器 — 将二进制帧转为可读摘要,用于日志输出
""" """
from typing import Any, cast from typing import Any, cast
from .taf_protocol import TafInputStream, TafType from .taf_protocol import TafInputStream, TafType
from .wup_protocol import normalize_wup_payload from .wup_protocol import normalize_wup_payload
@@ -103,7 +104,7 @@ def _decode_taf_struct(ins: TafInputStream, depth: int = 0) -> dict:
except Exception: except Exception:
val = f"<decode_err:0x{dtype:02x}>" val = f"<decode_err:0x{dtype:02x}>"
else: else:
val = f"<...>" val = "<...>"
try: try:
ins.skip_field(dtype) ins.skip_field(dtype)
except Exception: except Exception:
@@ -152,13 +153,7 @@ def _decode_wup_body(body: bytes) -> dict:
if tag > 10: if tag > 10:
break break
ins.read_head() ins.read_head()
if tag == 1: if tag == 1 or tag in (2, 3) or tag == 4:
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 dtype != TafType.ZERO: if dtype != TafType.ZERO:
ins._read_int_value(dtype) ins._read_int_value(dtype)
elif tag == 5: elif tag == 5:
+7 -7
View File
@@ -11,13 +11,13 @@ import base64
import hashlib import hashlib
import json import json
import random import random
import struct
import urllib.parse import urllib.parse
import urllib.request 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 .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 from .wup_protocol import WupRequest, WupResponse
CDNWS_HOST = "cdnws.api.huya.com" CDNWS_HOST = "cdnws.api.huya.com"
@@ -849,11 +849,11 @@ class HuyaHttpClient:
): ):
"""order_type=None 时自动尝试从 1 到 10 找到有效值""" """order_type=None 时自动尝试从 1 到 10 找到有效值"""
from .shop_structs import ( from .shop_structs import (
CreateOrderReqV5, CreateOrderAccountParam,
CreateOrderRsp,
CreateOrderExtraParam, CreateOrderExtraParam,
CreateOrderPromotionParam, CreateOrderPromotionParam,
CreateOrderAccountParam, CreateOrderReqV5,
CreateOrderRsp,
) )
# 如果指定了具体值,直接试 # 如果指定了具体值,直接试
@@ -951,7 +951,7 @@ class HuyaHttpClient:
baseinfo = self._generate_rpc_baseinfo(uid, guid, cookie) baseinfo = self._generate_rpc_baseinfo(uid, guid, cookie)
url = f"https://{CDNWS_HOST}/?baseinfo={baseinfo}" 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()}") self.logger(f"[HTTP] 发送 hex前60: {wup_data[:60].hex()}")
req = urllib.request.Request( req = urllib.request.Request(
+2 -3
View File
@@ -14,12 +14,11 @@ from http.cookies import SimpleCookie
from urllib.parse import quote, urlsplit, urlunsplit from urllib.parse import quote, urlsplit, urlunsplit
import requests import requests
from requests.cookies import RequestsCookieJar
from loguru import logger from loguru import logger
from requests.cookies import RequestsCookieJar
from .cookie_utils import normalize_huya_cookie from .cookie_utils import normalize_huya_cookie
APP_ID = "5002" APP_ID = "5002"
APP_VERSION = "2.6" APP_VERSION = "2.6"
APP_SIGN = "1ce3bf682483d03f146f58232ec10635" 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: def generate_request_id() -> str:
"""生成 requestId,形态参考旧实现的日内毫秒数。""" """生成 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) midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
return str(int((now - midnight).total_seconds() * 1000)) return str(int((now - midnight).total_seconds() * 1000))
+14 -14
View File
@@ -5,8 +5,8 @@
来源:m-shop.yaoguo.com 的 jce/ShopFacade.js、api/orderui.ts、api/mall/PlayMallNewHome.ts 来源: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: class OrderType:
@@ -387,7 +387,7 @@ class GoodsPriceInfo(TafStruct):
self.spuId: str = "" self.spuId: str = ""
self.minPrice: int = 0 self.minPrice: int = 0
self.maxPrice: int = 0 self.maxPrice: int = 0
self.skuMap: Dict[int, GoodsSkuItem] = {} self.skuMap: dict[int, GoodsSkuItem] = {}
self.stock: int = 0 self.stock: int = 0
self.buyLimit: int = 0 self.buyLimit: int = 0
self.price: int = 0 self.price: int = 0
@@ -500,7 +500,7 @@ class OrderListGoodsDetail(TafStruct):
self.buyerUid: int = 0 # tag 18 self.buyerUid: int = 0 # tag 18
self.virtualType: int = 0 # tag 19 self.virtualType: int = 0 # tag 19
self.quantity: int = 0 # tag 20 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 self.points: int = 0 # tag 23
def read_from(self, ins: TafInputStream): def read_from(self, ins: TafInputStream):
@@ -544,7 +544,7 @@ class OrderListItem(TafStruct):
self.totalPrice: int = 0 # tag 12 分 self.totalPrice: int = 0 # tag 12 分
self.createTime: int = 0 # tag 14 毫秒时间戳 self.createTime: int = 0 # tag 14 毫秒时间戳
self.payTime: int = 0 # tag 15 毫秒时间戳 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): def read_from(self, ins: TafInputStream):
self.bizOrderId = ins.read_string(0, default=self.bizOrderId) self.bizOrderId = ins.read_string(0, default=self.bizOrderId)
@@ -611,7 +611,7 @@ class QueryUserOrderListRsp(TafStruct):
self.code: int = 0 self.code: int = 0
self.message: str = "" self.message: str = ""
self.totalCount: int = 0 self.totalCount: int = 0
self.orders: List[OrderListItem] = [] self.orders: list[OrderListItem] = []
@staticmethod @staticmethod
def _read_order_item(ins: TafInputStream, _tag: int): def _read_order_item(ins: TafInputStream, _tag: int):
@@ -692,9 +692,9 @@ class CreateOrderPromotionParam(TafStruct):
class CreateOrderAccountParam(TafStruct): class CreateOrderAccountParam(TafStruct):
def __init__(self): 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.payoutChargeAmount: int = 0 # tag 1
self.cancelPayoutTypeList: List[int] = [] # tag 2 self.cancelPayoutTypeList: list[int] = [] # tag 2
self.recycleSupplierId: int = 0 # tag 3 self.recycleSupplierId: int = 0 # tag 3
self.claimPrice: int = 0 # tag 4 self.claimPrice: int = 0 # tag 4
@@ -739,20 +739,20 @@ class CreateOrderReqV5(TafStruct):
self.gameId: str = "" # tag 8 self.gameId: str = "" # tag 8
self.orderId: int = 0 # tag 9 self.orderId: int = 0 # tag 9
self.src: int = 0 # tag 10 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.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.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.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.orderScene: int = 0 # tag 18
self.watchWord: str = "" # tag 19 self.watchWord: str = "" # tag 19
self.marketingChannel: str = "" # tag 20 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.externalTraceKey: str = "" # tag 22
self.kefuUid: int = 0 # tag 23 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.parentOrderId: int = 0 # tag 25
self.vendorAccountType: str = "" # tag 26 self.vendorAccountType: str = "" # tag 26
self.vendorAccountVal: str = "" # tag 27 self.vendorAccountVal: str = "" # tag 27
+1 -2
View File
@@ -29,7 +29,6 @@ from .login import (
generate_request_id, generate_request_id,
) )
SMS_CODE_URI = "60027" SMS_CODE_URI = "60027"
SMS_LOGIN_URI = "60025" SMS_LOGIN_URI = "60025"
SMS_CODE_URL = "https://udblgn.huya.com/web/v2/smsCode" SMS_CODE_URL = "https://udblgn.huya.com/web/v2/smsCode"
@@ -604,7 +603,7 @@ class HuyaSmsLogin:
phone: str = "", phone: str = "",
proxies: Mapping[str, str] | None = None, proxies: Mapping[str, str] | None = None,
timeout: tuple[float, float] | None = None, timeout: tuple[float, float] | None = None,
) -> "HuyaSmsLogin": ) -> HuyaSmsLogin:
"""从发码阶段返回的 state 恢复短信登录会话。""" """从发码阶段返回的 state 恢复短信登录会话。"""
try: try:
raw = base64.urlsafe_b64decode(state.encode("ascii")) raw = base64.urlsafe_b64decode(state.encode("ascii"))
+10 -10
View File
@@ -9,9 +9,9 @@
0x0c ZERO 0x0d SIMPLE_LIST 0x0c ZERO 0x0d SIMPLE_LIST
""" """
import struct
import io import io
from typing import Any, Dict, List, Optional, Tuple import struct
from typing import Any
class TafType: class TafType:
@@ -136,7 +136,7 @@ class TafOutputStream:
# ---- Map ---- # ---- Map ----
def write_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_head(tag, TafType.MAP)
self.write_int32(0, len(value)) self.write_int32(0, len(value))
@@ -151,7 +151,7 @@ class TafOutputStream:
self._write_any(1, v) self._write_any(1, v)
# ---- List ---- # ---- 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_head(tag, TafType.LIST)
self.write_int32(0, len(value)) self.write_int32(0, len(value))
for item in value: for item in value:
@@ -192,7 +192,7 @@ class TafInputStream:
def __init__(self, data: bytes): def __init__(self, data: bytes):
self.buf = io.BytesIO(data) self.buf = io.BytesIO(data)
def peek_head(self) -> Tuple[int, int]: def peek_head(self) -> tuple[int, int]:
"""读取 head 但不消费(用于探测)""" """读取 head 但不消费(用于探测)"""
pos = self.buf.tell() pos = self.buf.tell()
try: try:
@@ -200,7 +200,7 @@ class TafInputStream:
finally: finally:
self.buf.seek(pos) self.buf.seek(pos)
def read_head(self) -> Tuple[int, int]: def read_head(self) -> tuple[int, int]:
"""返回 (tag, type)""" """返回 (tag, type)"""
data = self.buf.read(1) data = self.buf.read(1)
if not data: if not data:
@@ -293,7 +293,7 @@ class TafInputStream:
self.skip_field(it) self.skip_field(it)
# ---- 带跳过策略的字段读取:找到 tag,否则返回默认 ---- # ---- 带跳过策略的字段读取:找到 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""" """逐个读 head,tag 相等则返回,tag 超过则回退并返回 None"""
while True: while True:
pos = self.buf.tell() pos = self.buf.tell()
@@ -402,7 +402,7 @@ class TafInputStream:
# ---- 复合类型 ---- # ---- 复合类型 ----
def read_map( def read_map(
self, tag: int, required: bool = False, key_reader=None, val_reader=None self, tag: int, required: bool = False, key_reader=None, val_reader=None
) -> Dict: ) -> dict:
found = self._find_tag(tag, required) found = self._find_tag(tag, required)
if not found: if not found:
return {} return {}
@@ -418,7 +418,7 @@ class TafInputStream:
result[k] = v result[k] = v
return result 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) found = self._find_tag(tag, required)
if not found: if not found:
return [] return []
@@ -486,7 +486,7 @@ class TafStruct:
def read_from(self, ins: TafInputStream): def read_from(self, ins: TafInputStream):
raise NotImplementedError 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("_")} return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
-1
View File
@@ -13,7 +13,6 @@ import onnxruntime as ort
from PIL import Image from PIL import Image
from scipy.optimize import linear_sum_assignment from scipy.optimize import linear_sum_assignment
MODEL_DIR = Path(__file__).resolve().parent / "models" MODEL_DIR = Path(__file__).resolve().parent / "models"
+1 -2
View File
@@ -17,13 +17,12 @@ import cv2
import execjs import execjs
import numpy as np import numpy as np
import requests import requests
from PIL import Image
from loguru import logger from loguru import logger
from PIL import Image
from .ocr import HuyaCaptchaOcr, default_ocr from .ocr import HuyaCaptchaOcr, default_ocr
from .track import format_track, generate_slide_track from .track import format_track, generate_slide_track
DEFAULT_UA = ( DEFAULT_UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) " "AppleWebKit/537.36 (KHTML, like Gecko) "
+12 -12
View File
@@ -21,14 +21,14 @@ import re
import struct import struct
import time import time
from collections import deque from collections import deque
from typing import Any, Optional, Callable, cast from collections.abc import Callable
from typing import Any, cast
import websockets 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 .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" SHOP_WS_HOST = "77bc035c-ws.va.huya.com"
# 商城端点 baseinfo (conn4, h5_/index.html) — 商城业务 shopMiddleUI 走此通道 # 商城端点 baseinfo (conn4, h5_/index.html) — 商城业务 shopMiddleUI 走此通道
@@ -225,7 +225,7 @@ class HuyaWssClient:
), ),
timeout=timeout, timeout=timeout,
) )
except asyncio.TimeoutError: except TimeoutError:
self.logger(f"[WSS] 连接超时({timeout}s") self.logger(f"[WSS] 连接超时({timeout}s")
raise raise
except Exception as e: except Exception as e:
@@ -376,7 +376,7 @@ class HuyaWssClient:
try: try:
body = await asyncio.wait_for(future, timeout) body = await asyncio.wait_for(future, timeout)
except asyncio.TimeoutError: except TimeoutError:
if future in self.rpc_queue: if future in self.rpc_queue:
self.rpc_queue.remove(future) self.rpc_queue.remove(future)
self.logger("[✗] wsLaunch 超时") self.logger("[✗] wsLaunch 超时")
@@ -526,7 +526,7 @@ class HuyaWssClient:
try: try:
body = await asyncio.wait_for(future, timeout) body = await asyncio.wait_for(future, timeout)
except asyncio.TimeoutError: except TimeoutError:
if future in self.rpc_queue: if future in self.rpc_queue:
self.rpc_queue.remove(future) self.rpc_queue.remove(future)
self.logger("[✗] getConfig 超时") self.logger("[✗] getConfig 超时")
@@ -596,7 +596,7 @@ class HuyaWssClient:
try: try:
body = await asyncio.wait_for(future, timeout) body = await asyncio.wait_for(future, timeout)
except asyncio.TimeoutError: except TimeoutError:
if future in self.rpc_queue: if future in self.rpc_queue:
self.rpc_queue.remove(future) self.rpc_queue.remove(future)
self.logger(f"[✗] {service}.{method} 超时") self.logger(f"[✗] {service}.{method} 超时")
@@ -696,11 +696,11 @@ class HuyaWssClient:
order_type: int = 6, order_type: int = 6,
): ):
from .shop_structs import ( from .shop_structs import (
CreateOrderReqV5, CreateOrderAccountParam,
CreateOrderRsp,
CreateOrderExtraParam, CreateOrderExtraParam,
CreateOrderPromotionParam, CreateOrderPromotionParam,
CreateOrderAccountParam, CreateOrderReqV5,
CreateOrderRsp,
) )
req = CreateOrderReqV5() req = CreateOrderReqV5()
@@ -781,7 +781,7 @@ class HuyaWssClient:
try: try:
body = await asyncio.wait_for(future, 15.0) body = await asyncio.wait_for(future, 15.0)
except asyncio.TimeoutError: except TimeoutError:
if future in self.rpc_queue: if future in self.rpc_queue:
self.rpc_queue.remove(future) self.rpc_queue.remove(future)
self.logger("[✗] payOrderSubmitV5 超时") self.logger("[✗] payOrderSubmitV5 超时")
+4 -4
View File
@@ -9,7 +9,7 @@ import json
import random import random
import struct import struct
import time as _time import time as _time
from typing import Any, Dict from typing import Any
# TAF 类型标签 # TAF 类型标签
INT8, INT16, INT32, INT64 = 0x00, 0x01, 0x02, 0x03 INT8, INT16, INT32, INT64 = 0x00, 0x01, 0x02, 0x03
@@ -97,7 +97,7 @@ class _Writer:
def _build_meta_json(session: int, trace_id: str) -> str: def _build_meta_json(session: int, trace_id: str) -> str:
"""构造 _wup_data.t0.t2 元数据 JSON。""" """构造 _wup_data.t0.t2 元数据 JSON。"""
meta: Dict[str, Any] = { meta: dict[str, Any] = {
"associationId": 8193, "associationId": 8193,
"funcName": "hypasswordLogin", "funcName": "hypasswordLogin",
"group": 1, "group": 1,
@@ -167,7 +167,7 @@ def _build_wup_data(
session: int, session: int,
trace_id: str, trace_id: str,
user_action_json: str, user_action_json: str,
device_info: Dict[str, str], device_info: dict[str, str],
) -> None: ) -> None:
"""编码 _wup_data struct。""" """编码 _wup_data struct。"""
meta_json = _build_meta_json(session, trace_id) meta_json = _build_meta_json(session, trace_id)
@@ -235,7 +235,7 @@ def build_password_login_wup(
session: int, session: int,
trace_id: str, trace_id: str,
user_action_json: str, user_action_json: str,
device_info: Dict[str, str], device_info: dict[str, str],
) -> bytes: ) -> bytes:
"""构造密码登录的 WUP TAF 请求体。""" """构造密码登录的 WUP TAF 请求体。"""
wd = _Writer() wd = _Writer()
+10 -9
View File
@@ -11,8 +11,9 @@ Wup 包结构:
""" """
import struct import struct
from typing import Any, Dict, Optional from typing import Any
from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct
from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType
class WupRequest: class WupRequest:
@@ -27,9 +28,9 @@ class WupRequest:
self.sFuncName: str = "" # tag 6 self.sFuncName: str = "" # tag 6
self.sBuffer: bytes = b"" # tag 7 self.sBuffer: bytes = b"" # tag 7
self.iTimeout: int = 3000 # tag 8 self.iTimeout: int = 3000 # tag 8
self.context: Dict[str, str] = {} # tag 9 self.context: dict[str, str] = {} # tag 9
self.status: Dict[str, str] = {} # tag 10 self.status: dict[str, str] = {} # tag 10
self.newdata: Dict[str, bytes] = {} self.newdata: dict[str, bytes] = {}
def setServant(self, name: str): def setServant(self, name: str):
self.sServantName = name self.sServantName = name
@@ -141,9 +142,9 @@ class WupResponse:
self.sFuncName: str = "" self.sFuncName: str = ""
self.sBuffer: bytes = b"" self.sBuffer: bytes = b""
self.iTimeout: int = 0 self.iTimeout: int = 0
self.context: Dict[str, str] = {} self.context: dict[str, str] = {}
self.status: Dict[str, str] = {} self.status: dict[str, str] = {}
self.newdata: Dict[str, bytes] = {} self.newdata: dict[str, bytes] = {}
def decode(self, data: bytes): def decode(self, data: bytes):
"""解码响应(不包含长度前缀;若含前缀会自动跳过)""" """解码响应(不包含长度前缀;若含前缀会自动跳过)"""
@@ -269,7 +270,7 @@ def _read_bytes_value(ins: TafInputStream, dtype: int) -> bytes:
return ins.buf.read(length) 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: if dtype != TafType.MAP:
return {} return {}
count = ins._read_int_len() count = ins._read_int_len()
-1
View File
@@ -10,7 +10,6 @@ from urllib.parse import urlparse
import requests import requests
CODE_PATTERN = re.compile( CODE_PATTERN = re.compile(
r"(?:verification\s+code|验证码|校验码|动态码|安全码)\D{0,20}(\d{4,8})", r"(?:verification\s+code|验证码|校验码|动态码|安全码)\D{0,20}(\d{4,8})",
re.IGNORECASE, re.IGNORECASE,
+1 -2
View File
@@ -14,7 +14,6 @@ from sqlalchemy import JSON, MetaData, create_engine, func, inspect, select
from sqlalchemy.engine import Connection, Engine from sqlalchemy.engine import Connection, Engine
from sqlalchemy.schema import Table from sqlalchemy.schema import Table
PROJECT_ROOT = Path(__file__).resolve().parents[1] PROJECT_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_SOURCE = PROJECT_ROOT / "data" / "web.db" DEFAULT_SOURCE = PROJECT_ROOT / "data" / "web.db"
IGNORED_SOURCE_TABLES = {"alembic_version"} IGNORED_SOURCE_TABLES = {"alembic_version"}
@@ -147,7 +146,7 @@ def main() -> int:
if target_url: if target_url:
os.environ["DATABASE_URL"] = target_url os.environ["DATABASE_URL"] = target_url
from web.backend import models # noqa: F401 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 target_url = target_url or DATABASE_URL
if not target_url.startswith("mysql+"): if not target_url.startswith("mysql+"):
+8 -7
View File
@@ -34,19 +34,20 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from pyvm.algorithm import build_plaintext, generate_encrypt_msg_offline # noqa: E402 from pyvm.algorithm import generate_encrypt_msg_offline
from pyvm.mall import MallSession, generate_encrypt_msg as mall_generate # noqa: E402 from pyvm.login_profile import midas_login_params
from pyvm.session import SessionState, load_session # noqa: E402 from pyvm.mall import MallSession
from pyvm.login_profile import midas_login_params # noqa: E402 from pyvm.mall import generate_encrypt_msg as mall_generate
from pyvm.payment_errors import describe_payment_failure # noqa: E402 from pyvm.payment_errors import describe_payment_failure
from pyvm.protocol import ( # noqa: E402 from pyvm.protocol import (
GOODS_USER_AGENT,
MALL_API_URL, MALL_API_URL,
MALL_USER_AGENT, MALL_USER_AGENT,
GOODS_USER_AGENT,
PAY_APPID, PAY_APPID,
validate_goods_materials, validate_goods_materials,
validate_mall_materials, validate_mall_materials,
) )
from pyvm.session import SessionState, load_session
REPLAY = ROOT / "replay" REPLAY = ROOT / "replay"
DEFAULT_APPID = PAY_APPID DEFAULT_APPID = PAY_APPID
+10 -8
View File
@@ -13,22 +13,24 @@ from .algorithm import (
generate_encrypt_msg, generate_encrypt_msg,
generate_encrypt_msg_offline, generate_encrypt_msg_offline,
) )
from .goods import GoodsSession, generate_encrypt_msg as goods_generate from .goods import GoodsSession
from .mall import MallSession, generate_encrypt_msg as mall_generate 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 .pagedoo_vm import PagedooVM, run_frame
from .session import SessionState, load_session from .session import SessionState, load_session
__all__ = [ __all__ = [
"GoodsSession",
"MallSession",
"PagedooVM",
"SessionState",
"build_plaintext", "build_plaintext",
"decode_d", "decode_d",
"generate_encrypt_msg", "generate_encrypt_msg",
"generate_encrypt_msg_offline", "generate_encrypt_msg_offline",
"GoodsSession",
"goods_generate", "goods_generate",
"MallSession",
"mall_generate",
"PagedooVM",
"run_frame",
"SessionState",
"load_session", "load_session",
"mall_generate",
"run_frame",
] ]
@@ -13,6 +13,7 @@
""" """
from __future__ import annotations from __future__ import annotations
import itertools import itertools
import json import json
import math import math
@@ -355,7 +356,7 @@ def js_set(obj, key, val):
if isinstance(obj, str): if isinstance(obj, str):
raise TypeError("Cannot assign to read only property") raise TypeError("Cannot assign to read only property")
cur = getattr( cur = getattr(
getattr(__import__("pyvm.algorithm", fromlist=["x"]), "VM"), "_last_u", None __import__("pyvm.algorithm", fromlist=["x"]).VM, "_last_u", None
) )
raise TypeError( raise TypeError(
f"invalid set target obj={type(obj).__name__} key={key!r} val={type(val).__name__} u={cur}" 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: class JSFunction:
"""VM 函数(解释器实例工厂返回的 h)。""" """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): def __init__(self, vm, entry, args, s, n, t):
self.vm = vm self.vm = vm
@@ -455,7 +456,7 @@ def js_call(fn, this, args):
return fn.fn(this, *args) return fn.fn(this, *args)
if callable(fn): if callable(fn):
return fn(this, *args) 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): def js_apply(fn, this, args):
+2 -3
View File
@@ -12,7 +12,6 @@ from __future__ import annotations
import json import json
from pathlib import Path from pathlib import Path
from typing import Any
from .algorithm import generate_encrypt_msg_offline from .algorithm import generate_encrypt_msg_offline
@@ -64,7 +63,7 @@ class GoodsSession:
raise ValueError("key16/key1 应为 16 字节") raise ValueError("key16/key1 应为 16 字节")
@classmethod @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 加载。""" """从 scripts/capture-session.mjs 生成的 session-state.json 加载。"""
d = json.loads(Path(path).read_text(encoding="utf-8")) d = json.loads(Path(path).read_text(encoding="utf-8"))
return cls( return cls(
@@ -85,7 +84,7 @@ class GoodsSession:
} }
@classmethod @classmethod
def from_json(cls, d: dict) -> "GoodsSession": def from_json(cls, d: dict) -> GoodsSession:
return cls( return cls(
d["xmidas_ops"], d["xmidas_ops"],
d["key16"], d["key16"],
+2 -3
View File
@@ -19,7 +19,6 @@ import copy
import json import json
import re import re
from pathlib import Path from pathlib import Path
from typing import Any
from .algorithm import UNDEF, JSObject, Window from .algorithm import UNDEF, JSObject, Window
from .pagedoo_vm import PagedooVM from .pagedoo_vm import PagedooVM
@@ -78,7 +77,7 @@ class MallSession:
raise ValueError(f"624B 中间态长度 != 624: {len(mid[0])}") raise ValueError(f"624B 中间态长度 != 624: {len(mid[0])}")
@classmethod @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 提取同会话 transform_input + xMidasOps。
frames.jsonl scripts/capture-mall-session.mjs 实时落盘: frames.jsonl scripts/capture-mall-session.mjs 实时落盘:
@@ -127,7 +126,7 @@ class MallSession:
} }
@classmethod @classmethod
def from_json(cls, d: dict) -> "MallSession": def from_json(cls, d: dict) -> MallSession:
return cls(d["transform_input"], d["xmidas_ops"]) return cls(d["transform_input"], d["xmidas_ops"])
+34 -39
View File
@@ -13,56 +13,51 @@ JS 语义辅助复用 algorithm.py(_ic/i32/js_add/js_index/JSObject/JSFunction
from __future__ import annotations from __future__ import annotations
import json import json
import math
import urllib.parse
from pathlib import Path
from .algorithm import ( from .algorithm import (
JSDate,
UNDEF, UNDEF,
HostFunction,
JSDate,
JSFunction, JSFunction,
JSObject, JSObject,
HostFunction,
_ic, _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_decodeuri,
h_decodeuricomponent, 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_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 pathlib import Path
from typing import Any from typing import Any
PAY_APPID = "1450243039" PAY_APPID = "1450243039"
PAY_GOODS_URL = "https://pay.qq.com/midas/minipay_v2/views/cpay/goods.shtml" 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" PAY_SAVE_URL = f"https://api.unipay.qq.com/v1/r/{PAY_APPID}/web_save"
+1 -1
View File
@@ -68,7 +68,7 @@ class SessionState:
} }
@classmethod @classmethod
def from_json(cls, d: dict) -> "SessionState": def from_json(cls, d: dict) -> SessionState:
return cls( return cls(
xmidas_ops=list(d.get("xmidas_ops", [])), xmidas_ops=list(d.get("xmidas_ops", [])),
key16=list(d.get("key16", [])), key16=list(d.get("key16", [])),
@@ -31,10 +31,10 @@ from pyvm.algorithm import (
build_plaintext, build_plaintext,
derive_key1_from_key16, derive_key1_from_key16,
generate_encrypt_msg_offline, generate_encrypt_msg_offline,
) # noqa: E402 )
from pyvm.login_profile import midas_login_params # noqa: E402 from pyvm.login_profile import midas_login_params
from pyvm.payment_errors import describe_payment_failure # noqa: E402 from pyvm.payment_errors import describe_payment_failure
from pyvm.protocol import ( # noqa: E402 from pyvm.protocol import (
DEFAULT_ORDER_PF, DEFAULT_ORDER_PF,
GOODS_USER_AGENT, GOODS_USER_AGENT,
PAY_APPID, PAY_APPID,
@@ -450,7 +450,6 @@ def cmd_check_only(session_path: Path, out_dir: Path) -> int:
from pyvm.order_status import ( from pyvm.order_status import (
completion_summary, completion_summary,
get_official_orders, get_official_orders,
is_finished,
) )
session = load_json(session_path) session = load_json(session_path)
@@ -697,7 +696,7 @@ def main() -> int:
out_dir, fields, int(fp.get("device_fp_length", 0)), plaintext_length out_dir, fields, int(fp.get("device_fp_length", 0)), plaintext_length
) )
body = urllib.parse.urlencode(fields) 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 baseline_document = None
@@ -17,8 +17,8 @@ from pathlib import Path
from urllib.error import HTTPError from urllib.error import HTTPError
from urllib.parse import urlencode from urllib.parse import urlencode
from urllib.request import ( from urllib.request import (
HTTPRedirectHandler,
HTTPCookieProcessor, HTTPCookieProcessor,
HTTPRedirectHandler,
Request, Request,
build_opener, build_opener,
) )
@@ -43,7 +43,7 @@ USER_AGENT = (
class NoRedirect(HTTPRedirectHandler): class NoRedirect(HTTPRedirectHandler):
"""Keep OAuth redirects visible while CookieJar receives Set-Cookie headers.""" """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 return None
@@ -118,7 +118,7 @@ def g_tk(p_skey: str) -> int:
def parse_poll(body: str) -> tuple[int, str]: 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: if not match:
raise ValueError("QQ 二维码轮询响应缺少 ptuiCB") raise ValueError("QQ 二维码轮询响应缺少 ptuiCB")
values = re.findall(r"'([^']*)'", match.group(1)) values = re.findall(r"'([^']*)'", match.group(1))
@@ -20,8 +20,8 @@ from pathlib import Path
from urllib.error import HTTPError from urllib.error import HTTPError
from urllib.parse import urlencode from urllib.parse import urlencode
from urllib.request import ( from urllib.request import (
HTTPRedirectHandler,
HTTPCookieProcessor, HTTPCookieProcessor,
HTTPRedirectHandler,
Request, Request,
build_opener, build_opener,
) )
@@ -42,7 +42,7 @@ USER_AGENT = (
class NoRedirect(HTTPRedirectHandler): class NoRedirect(HTTPRedirectHandler):
"""保留 OAuth 回调的 302 和 Set-Cookie。""" """保留 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 return None
@@ -88,7 +88,7 @@ def extract_uuid(page: str) -> str:
r"connect/qrcode/([A-Za-z0-9_-]{8,64})", r"connect/qrcode/([A-Za-z0-9_-]{8,64})",
r"uuid=([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: if match:
return match.group(1) return match.group(1)
raise ValueError("微信授权页未找到二维码 UUID") raise ValueError("微信授权页未找到二维码 UUID")
@@ -18,7 +18,7 @@ from curl_cffi import requests
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT)) 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 YYB_APP_ID = 52575843
SOURCE_ID = "24292013" SOURCE_ID = "24292013"
@@ -32,7 +32,7 @@ WORKER_KEY = os.environ.get("YYB_WORKER_KEY", "")
_jobs: dict[str, dict] = {} _jobs: dict[str, dict] = {}
_lock = threading.Lock() _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: 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"HOLD_[A-Za-z0-9_-]+", "[订单已隐藏]", clean)
clean = re.sub(r"(订单\s*[:]\s*)\S+", r"\1[订单已隐藏]", clean) clean = re.sub(r"(订单\s*[:]\s*)\S+", r"\1[订单已隐藏]", clean)
clean = re.sub( 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( clean = re.sub(
r"(?:pay_token|web_token|anti_token|session_id|sessionid)\s*[:= ]\s*\S+", r"(?:pay_token|web_token|anti_token|session_id|sessionid)\s*[:= ]\s*\S+",
"[敏感字段已隐藏]", "[敏感字段已隐藏]",
clean, clean,
flags=re.I, flags=re.IGNORECASE,
) )
timestamp = time.strftime("%H:%M:%S") timestamp = time.strftime("%H:%M:%S")
with _lock: with _lock:
@@ -523,7 +523,7 @@ class Handler(BaseHTTPRequestHandler):
value = self.headers.get("Authorization", "") value = self.headers.get("Authorization", "")
return value == f"Bearer {WORKER_KEY}" return value == f"Bearer {WORKER_KEY}"
def do_POST(self) -> None: # noqa: N802 def do_POST(self) -> None:
if not self._authorized(): if not self._authorized():
return self._json(401, {"detail": "未授权"}) return self._json(401, {"detail": "未授权"})
path = urlparse(self.path).path.strip("/").split("/") path = urlparse(self.path).path.strip("/").split("/")
@@ -687,7 +687,7 @@ class Handler(BaseHTTPRequestHandler):
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
return self._json(500, {"detail": str(exc)}) 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": if urlparse(self.path).path == "/health":
return self._json(200, {"status": "ok"}) return self._json(200, {"status": "ok"})
if not self._authorized(): if not self._authorized():
@@ -8,7 +8,6 @@ import unittest
from pathlib import Path from pathlib import Path
from urllib.parse import parse_qs from urllib.parse import parse_qs
_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "jsdom-pay.py" _SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "jsdom-pay.py"
_SPEC = importlib.util.spec_from_file_location("yyb_jsdom_pay", _SCRIPT) _SPEC = importlib.util.spec_from_file_location("yyb_jsdom_pay", _SCRIPT)
assert _SPEC and _SPEC.loader assert _SPEC and _SPEC.loader
@@ -16,8 +15,8 @@ _MODULE = importlib.util.module_from_spec(_SPEC)
sys.modules[_SPEC.name] = _MODULE sys.modules[_SPEC.name] = _MODULE
_SPEC.loader.exec_module(_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.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 from pyvm.protocol import goods_material_diagnostics, validate_goods_materials
+1 -2
View File
@@ -1,8 +1,7 @@
import pytest import pytest
from fastapi import HTTPException
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from fastapi import HTTPException
from web.backend.database import Base from web.backend.database import Base
from web.backend.models import Account, User from web.backend.models import Account, User
+1 -2
View File
@@ -1,9 +1,8 @@
"""充值审计日志的权限、查询和脱敏测试。""" """充值审计日志的权限、查询和脱敏测试。"""
import pytest
import json import json
import pytest
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -17,7 +17,7 @@ class TestCustomCookieOrder:
self.db.add(self.user) self.db.add(self.user)
self.db.flush() self.db.flush()
now = datetime.now(timezone.utc) now = datetime.now(UTC)
accounts = { accounts = {
name: Account(username=name, password="p", email="e", email_password="ep") name: Account(username=name, password="p", email="e", email_password="ep")
for name in ("account-a", "account-b", "account-c") for name in ("account-a", "account-b", "account-c")
+7 -7
View File
@@ -1,18 +1,13 @@
import pytest
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import patch
from typing import cast from typing import cast
from unittest.mock import patch
import pytest
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from web.backend.database import Base from web.backend.database import Base
from web.backend.models import Account, LoginTask, ProxyConfig, User 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 ( from web.backend.routers.cookies import (
check_cookie_operations, check_cookie_operations,
get_cookie, get_cookie,
@@ -21,6 +16,11 @@ from web.backend.routers.cookies import (
list_cookies, list_cookies,
relogin_invalid_cookie_operations, 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: class TestCookieOperation:
+2 -2
View File
@@ -1,9 +1,9 @@
"""精英手册兑换:csrf 复用、新链路接口与浏览器状态机路由的测试。""" """精英手册兑换:csrf 复用、新链路接口与浏览器状态机路由的测试。"""
import pytest
from unittest.mock import Mock, call from unittest.mock import Mock, call
import pytest
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
+1 -1
View File
@@ -1,6 +1,6 @@
import pytest
from unittest.mock import Mock from unittest.mock import Mock
import pytest
import requests import requests
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
+2 -2
View File
@@ -7,13 +7,13 @@ from unittest.mock import ANY, AsyncMock, Mock, patch
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from core.douyu import FishFinRechargeClient, FishFinRechargeConfig
from web.backend.database import Base from web.backend.database import Base
from web.backend.models import Account, AuditLog, DouyuTask, LoginTask, User 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.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_runner import DouyuBatchRunner
from web.backend.services.douyu_service import ensure_douyu_config 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: class TestDouyuGoldRechargeChannel:
+1 -1
View File
@@ -1,6 +1,6 @@
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import Mock
from typing import Any, cast from typing import Any, cast
from unittest.mock import Mock
from core.douyu.login import DouyuLogin from core.douyu.login import DouyuLogin
from core.douyu.login_api_wgapi import WgapiLoginAPI from core.douyu.login_api_wgapi import WgapiLoginAPI
+2 -2
View File
@@ -3,12 +3,12 @@
from unittest.mock import Mock from unittest.mock import Mock
from core.douyu.activity_client import DouyuActivityClient 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 import DouyuBatchRunner
from web.backend.services.douyu_runner_core import ( from web.backend.services.douyu_runner_core import (
DouyuBatchRunnerCore,
DOUYU_PROXY_TASK_TYPES, DOUYU_PROXY_TASK_TYPES,
DouyuBatchRunnerCore,
) )
from web.backend.models import ProxyConfig
def _cfg(**overrides) -> ProxyConfig: def _cfg(**overrides) -> ProxyConfig:
-1
View File
@@ -1,5 +1,4 @@
import pytest import pytest
from sqlalchemy import create_engine, event from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
+2 -1
View File
@@ -1,9 +1,10 @@
import pytest
import json import json
import re import re
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import Mock from unittest.mock import Mock
import pytest
from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError
+2 -2
View File
@@ -1,9 +1,9 @@
"""鱼翅直充供应商 API 客户端测试。""" """鱼翅直充供应商 API 客户端测试。"""
import pytest
from unittest.mock import Mock from unittest.mock import Mock
import pytest
from core.douyu.recharge_api import ( from core.douyu.recharge_api import (
FishFinRechargeClient, FishFinRechargeClient,
FishFinRechargeConfig, FishFinRechargeConfig,
+9 -17
View File
@@ -1,21 +1,15 @@
"""虎牙 App 密码登录及相关组件测试。""" """虎牙 App 密码登录及相关组件测试。"""
import pytest
import base64
import os import os
import struct import struct
from unittest.mock import patch, MagicMock from unittest.mock import MagicMock, patch
import pytest
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from core.huya import ( from core.huya import (
HuyaAppLoginError, HuyaAppLoginError,
HuyaAppPasswordLogin,
HuyaAppQrAuthRequiredError,
login_huya_app_password,
login_huya_password,
) )
from core.huya.app_login import ( from core.huya.app_login import (
DEFAULT_GOLDEN_DEV, 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.nonce_forge import K1_DEFAULT, gen_nonce
from core.huya.udb_aes import udb_decrypt, udb_encrypt from core.huya.udb_aes import udb_decrypt, udb_encrypt
from core.huya.wup_encoder import build_password_login_wup from core.huya.wup_encoder import build_password_login_wup
from web.backend.database import Base from web.backend.database import Base
from web.backend.models import User, HuyaAccount from web.backend.models import HuyaAccount, User
from web.backend.schemas import (
HuyaAppPasswordLoginRequest,
HuyaPasswordLoginSelectedRequest,
)
from web.backend.routers.huya import ( from web.backend.routers.huya import (
app_password_login_account, app_password_login_account,
app_password_login_selected_accounts, app_password_login_selected_accounts,
) )
from web.backend.schemas import (
HuyaAppPasswordLoginRequest,
HuyaPasswordLoginSelectedRequest,
)
class TestHuyaAppLogin: class TestHuyaAppLogin:
@@ -193,9 +186,8 @@ class TestHuyaAppLogin:
with patch( with patch(
"core.huya.app_login.register_device", "core.huya.app_login.register_device",
side_effect=DfpRegistrationError("注册链 HTTP 500"), side_effect=DfpRegistrationError("注册链 HTTP 500"),
): ), pytest.raises(HuyaAppLoginError, match="注册失败"):
with pytest.raises(HuyaAppLoginError, match="注册失败"): login_cred_with_flow("300023887", "pw")
login_cred_with_flow("300023887", "pw")
def test_router_functions(self): def test_router_functions(self):
mock_res = HuyaLoginResult( mock_res = HuyaLoginResult(
+1 -1
View File
@@ -7,9 +7,9 @@ from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from utils.logger import ( from utils.logger import (
_parse_size,
_SensitiveDataFilter, _SensitiveDataFilter,
_SizeAndDayRotatingFileHandler, _SizeAndDayRotatingFileHandler,
_parse_size,
) )
+1 -1
View File
@@ -68,8 +68,8 @@ class TestMigrationSmoke:
def test_full_chain_applies_on_fresh_sqlite_and_matches_models(self): 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 import web.backend.database as database_module
from web.backend.database import Base
original_url = database_module.DATABASE_URL original_url = database_module.DATABASE_URL
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
+3 -4
View File
@@ -14,7 +14,6 @@ from pathlib import Path
from loguru import logger from loguru import logger
_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} _LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
_DEFAULT_RETENTION_DAYS = 14 _DEFAULT_RETENTION_DAYS = 14
_DEFAULT_ROTATION_SIZE = 50 * 1024 * 1024 _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: def _parse_size(value: str | None, default: int = _DEFAULT_ROTATION_SIZE) -> int:
"""解析 50M、1GiB 等易读大小;非法值保持安全默认值。""" """解析 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: if not matched:
return default return default
amount = int(matched.group(1)) amount = int(matched.group(1))
@@ -113,7 +112,7 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler):
self._log_dir / f"{self._filename_prefix}-{day.isoformat()}{self._suffix}" 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: if datetime.now().date() != self._active_day:
return True return True
if self.stream is None: if self.stream is None:
@@ -122,7 +121,7 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler):
message = f"{self.format(record)}\n" message = f"{self.format(record)}\n"
return self.stream.tell() + len(message.encode("utf-8")) >= self.max_bytes 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: if self.stream is not None:
self.stream.close() self.stream.close()
self.stream = None self.stream = None
+1 -2
View File
@@ -10,11 +10,10 @@ from dataclasses import dataclass
from functools import lru_cache from functools import lru_cache
from Crypto.Cipher import AES from Crypto.Cipher import AES
from loguru import logger
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlalchemy.types import Text, TypeDecorator from sqlalchemy.types import Text, TypeDecorator
from loguru import logger
_PREFIX = "enc:v1:" _PREFIX = "enc:v1:"
_FALLBACK_SECRET = "douyu-login-py-dev-storage-key-change-me" _FALLBACK_SECRET = "douyu-login-py-dev-storage-key-change-me"
+2 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
from urllib.parse import quote_plus from urllib.parse import quote_plus
from sqlalchemy import create_engine, event 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] PROJECT_ROOT = Path(__file__).resolve().parents[2]
@@ -147,6 +147,7 @@ def _seed():
def _encrypt_existing_sensitive_data(): def _encrypt_existing_sensitive_data():
"""启动时把历史明文敏感数据迁移为密文。""" """启动时把历史明文敏感数据迁移为密文。"""
from loguru import logger from loguru import logger
from .crypto_storage import encrypt_existing_sensitive_data from .crypto_storage import encrypt_existing_sensitive_data
changed = encrypt_existing_sensitive_data(engine) changed = encrypt_existing_sensitive_data(engine)
+9 -9
View File
@@ -1,15 +1,15 @@
"""FastAPI 依赖注入""" """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 fastapi import Depends, HTTPException, Request, WebSocket, status
from .security import decode_access_token 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 .models import User
from .permissions import get_user_permissions from .permissions import get_user_permissions
from .security import decode_access_token
# auto_error=False: 允许 token 为空(后续从 cookie 读取) # auto_error=False: 允许 token 为空(后续从 cookie 读取)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False) 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( def get_current_user(
request: Request, request: Request,
token: Optional[str] = Depends(oauth2_scheme), token: str | None = Depends(oauth2_scheme),
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> User: ) -> User:
credentials_exc = HTTPException( credentials_exc = HTTPException(
@@ -61,7 +61,7 @@ def require_permission(permission: str):
return checker return checker
def authenticate_websocket(websocket: WebSocket) -> Optional[User]: def authenticate_websocket(websocket: WebSocket) -> User | None:
"""WebSocket 认证:从 cookie 或 query param token 中验证用户身份。 """WebSocket 认证:从 cookie 或 query param token 中验证用户身份。
Returns: Returns:
+15 -13
View File
@@ -1,34 +1,36 @@
"""FastAPI 入口""" """FastAPI 入口"""
import os import os
from pathlib import Path
import uvicorn
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path
import uvicorn
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from utils import setup_logger
from .database import init_db from .database import init_db
from .routers import ( from .routers import (
auth,
users,
accounts,
account_check, account_check,
accounts,
audit,
auth,
cookies,
dashboard, dashboard,
douyu,
huya,
login, login,
proxy, proxy,
cookies, users,
huya,
douyu,
yyb, yyb,
audit,
) )
from .schemas import AppInfo from .schemas import AppInfo
from .version import get_app_version from .version import get_app_version
from utils import setup_logger
@asynccontextmanager @asynccontextmanager
@@ -45,9 +47,9 @@ async def lifespan(app: FastAPI):
from loguru import logger from loguru import logger
from .database import SessionLocal 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.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() db = SessionLocal()
try: try:
+4 -4
View File
@@ -1,9 +1,9 @@
"""Alembic 迁移环境。""" """Alembic 迁移环境。"""
from logging.config import fileConfig
from pathlib import Path
import os import os
import sys import sys
from logging.config import fileConfig
from pathlib import Path
from alembic import context from alembic import context
from sqlalchemy import engine_from_config, pool 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: if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR)) sys.path.insert(0, str(ROOT_DIR))
from web.backend.database import Base, DATABASE_URL # noqa: E402 from web.backend import models # noqa: F401
from web.backend import models # noqa: F401,E402 from web.backend.database import DATABASE_URL, Base
config = context.config config = context.config
config.set_main_option("sqlalchemy.url", DATABASE_URL) config.set_main_option("sqlalchemy.url", DATABASE_URL)
@@ -5,15 +5,15 @@ Revises:
Create Date: 2026-06-23 Create Date: 2026-06-23
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260623_0001" revision: str = "20260623_0001"
down_revision: Union[str, None] = None down_revision: str | None = None
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _has_table(bind, table_name: str) -> bool: def _has_table(bind, table_name: str) -> bool:
@@ -5,16 +5,15 @@ Revises: 20260623_0001
Create Date: 2026-06-24 Create Date: 2026-06-24
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260624_0002" revision: str = "20260624_0002"
down_revision: Union[str, None] = "20260623_0001" down_revision: str | None = "20260623_0001"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def upgrade() -> None: def upgrade() -> None:
@@ -5,16 +5,15 @@ Revises: 20260624_0002
Create Date: 2026-06-24 Create Date: 2026-06-24
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260624_0003" revision: str = "20260624_0003"
down_revision: Union[str, None] = "20260624_0002" down_revision: str | None = "20260624_0002"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def upgrade() -> None: def upgrade() -> None:
@@ -5,16 +5,15 @@ Revises: 20260624_0003
Create Date: 2026-07-04 Create Date: 2026-07-04
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260704_0004" revision: str = "20260704_0004"
down_revision: Union[str, None] = "20260624_0003" down_revision: str | None = "20260624_0003"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _has_table(bind, table_name: str) -> bool: def _has_table(bind, table_name: str) -> bool:
@@ -5,16 +5,15 @@ Revises: 20260704_0004
Create Date: 2026-07-04 Create Date: 2026-07-04
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260704_0005" revision: str = "20260704_0005"
down_revision: Union[str, None] = "20260704_0004" down_revision: str | None = "20260704_0004"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _has_table(bind, table_name: str) -> bool: def _has_table(bind, table_name: str) -> bool:
@@ -5,16 +5,15 @@ Revises: 20260704_0005
Create Date: 2026-07-05 Create Date: 2026-07-05
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260705_0006" revision: str = "20260705_0006"
down_revision: Union[str, None] = "20260704_0005" down_revision: str | None = "20260704_0005"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _has_table(bind, table_name: str) -> bool: def _has_table(bind, table_name: str) -> bool:
@@ -5,16 +5,15 @@ Revises: 20260705_0006
Create Date: 2026-07-12 Create Date: 2026-07-12
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260712_0007" revision: str = "20260712_0007"
down_revision: Union[str, None] = "20260705_0006" down_revision: str | None = "20260705_0006"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _has_table(bind, table_name: str) -> bool: def _has_table(bind, table_name: str) -> bool:
@@ -5,16 +5,15 @@ Revises: 20260712_0007
Create Date: 2026-07-24 Create Date: 2026-07-24
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260724_0008" revision: str = "20260724_0008"
down_revision: Union[str, None] = "20260712_0007" down_revision: str | None = "20260712_0007"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _has_table(bind, table_name: str) -> bool: def _has_table(bind, table_name: str) -> bool:
@@ -5,16 +5,15 @@ Revises: 20260724_0008
Create Date: 2026-07-25 Create Date: 2026-07-25
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260725_0009" revision: str = "20260725_0009"
down_revision: Union[str, None] = "20260724_0008" down_revision: str | None = "20260724_0008"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _has_table(bind, table_name: str) -> bool: def _has_table(bind, table_name: str) -> bool:
@@ -5,16 +5,15 @@ Revises: 20260725_0009
Create Date: 2026-07-28 Create Date: 2026-07-28
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260728_0010" revision: str = "20260728_0010"
down_revision: Union[str, None] = "20260725_0009" down_revision: str | None = "20260725_0009"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _columns(bind, table_name: str) -> set[str]: def _columns(bind, table_name: str) -> set[str]:
@@ -5,16 +5,15 @@ Revises: 20260728_0010
Create Date: 2026-07-28 Create Date: 2026-07-28
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260728_0011" revision: str = "20260728_0011"
down_revision: Union[str, None] = "20260728_0010" down_revision: str | None = "20260728_0010"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _columns(bind, table_name: str) -> set[str]: def _columns(bind, table_name: str) -> set[str]:
@@ -5,16 +5,15 @@ Revises: 20260728_0011
Create Date: 2026-07-28 Create Date: 2026-07-28
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260728_0012" revision: str = "20260728_0012"
down_revision: Union[str, None] = "20260728_0011" down_revision: str | None = "20260728_0011"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def upgrade() -> None: def upgrade() -> None:
@@ -5,16 +5,15 @@ Revises: 20260728_0012
Create Date: 2026-07-28 Create Date: 2026-07-28
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260728_0013" revision: str = "20260728_0013"
down_revision: Union[str, None] = "20260728_0012" down_revision: str | None = "20260728_0012"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _columns(bind, table_name: str) -> set[str]: def _columns(bind, table_name: str) -> set[str]:
@@ -5,16 +5,15 @@ Revises: 20260728_0013
Create Date: 2026-07-28 Create Date: 2026-07-28
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260728_0014" revision: str = "20260728_0014"
down_revision: Union[str, None] = "20260728_0013" down_revision: str | None = "20260728_0013"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def upgrade() -> None: def upgrade() -> None:
@@ -5,16 +5,15 @@ Revises: 20260728_0014
Create Date: 2026-08-05 Create Date: 2026-08-05
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260805_0015" revision: str = "20260805_0015"
down_revision: Union[str, None] = "20260728_0014" down_revision: str | None = "20260728_0014"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
INDEXES = [ INDEXES = [
@@ -5,16 +5,15 @@ Revises: 20260805_0015
Create Date: 2026-08-06 Create Date: 2026-08-06
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260806_0016" revision: str = "20260806_0016"
down_revision: Union[str, None] = "20260805_0015" down_revision: str | None = "20260805_0015"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _columns(bind, table_name: str) -> set[str]: def _columns(bind, table_name: str) -> set[str]:
@@ -5,16 +5,15 @@ Revises: 20260806_0016
Create Date: 2026-08-07 Create Date: 2026-08-07
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260807_0017" revision: str = "20260807_0017"
down_revision: Union[str, None] = "20260806_0016" down_revision: str | None = "20260806_0016"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def upgrade() -> None: def upgrade() -> None:
@@ -5,16 +5,15 @@ Revises: 20260807_0017
Create Date: 2026-08-07 Create Date: 2026-08-07
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260807_0018" revision: str = "20260807_0018"
down_revision: Union[str, None] = "20260807_0017" down_revision: str | None = "20260807_0017"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def upgrade() -> None: def upgrade() -> None:
@@ -5,16 +5,15 @@ Revises: 20260807_0018
Create Date: 2026-08-08 Create Date: 2026-08-08
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260808_0019" revision: str = "20260808_0019"
down_revision: Union[str, None] = "20260807_0018" down_revision: str | None = "20260807_0018"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def upgrade() -> None: def upgrade() -> None:
@@ -1,13 +1,14 @@
"""增加应用宝和平精英充值任务""" """增加应用宝和平精英充值任务"""
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260812_0020" revision: str = "20260812_0020"
down_revision: Union[str, None] = "20260808_0019" down_revision: str | None = "20260808_0019"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def upgrade() -> None: def upgrade() -> None:
@@ -1,15 +1,13 @@
"""扩展应用宝二维码密文字段容量""" """扩展应用宝二维码密文字段容量"""
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op from alembic import op
import sqlalchemy as sa
revision: str = "20260812_0021" revision: str = "20260812_0021"
down_revision: Union[str, None] = "20260812_0020" down_revision: str | None = "20260812_0020"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _is_mysql() -> bool: 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 import sqlalchemy as sa
from alembic import op
revision: str = "20260812_0022" revision: str = "20260812_0022"
down_revision: Union[str, None] = "20260812_0021" down_revision: str | None = "20260812_0021"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def upgrade() -> 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 import sqlalchemy as sa
from alembic import op
revision: str = "20260813_0023" revision: str = "20260813_0023"
down_revision: Union[str, None] = "20260812_0022" down_revision: str | None = "20260812_0022"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _columns(bind, table_name: str) -> set[str]: def _columns(bind, table_name: str) -> set[str]:
@@ -5,16 +5,15 @@ Revises: 20260813_0023
Create Date: 2026-08-13 Create Date: 2026-08-13
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260813_0024" revision: str = "20260813_0024"
down_revision: Union[str, None] = "20260813_0023" down_revision: str | None = "20260813_0023"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def _columns(bind, table_name: str) -> set[str]: def _columns(bind, table_name: str) -> set[str]:
@@ -5,16 +5,15 @@ Revises: 20260813_0024
Create Date: 2026-08-13 Create Date: 2026-08-13
""" """
from typing import Sequence, Union from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
revision: str = "20260813_0025" revision: str = "20260813_0025"
down_revision: Union[str, None] = "20260813_0024" down_revision: str | None = "20260813_0024"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: str | Sequence[str] | None = None
depends_on: Union[str, Sequence[str], None] = None depends_on: str | Sequence[str] | None = None
def upgrade() -> None: def upgrade() -> None:

Some files were not shown because too many files have changed in this diff Show More