type: 收窄虎牙 HTTP 与 WSS 客户端类型
This commit is contained in:
+235
-89
@@ -6,6 +6,7 @@
|
|||||||
Body: Wup 包 (含4字节长度前缀)
|
Body: Wup 包 (含4字节长度前缀)
|
||||||
Response: Wup 包 (tRsp)
|
Response: Wup 包 (tRsp)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
@@ -13,7 +14,7 @@ import random
|
|||||||
import struct
|
import struct
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from typing import Optional, Callable
|
from typing import Any, Optional, Callable
|
||||||
|
|
||||||
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 TafOutputStream, TafInputStream, TafType, TafStruct
|
||||||
@@ -21,9 +22,11 @@ from .wup_protocol import WupRequest, WupResponse
|
|||||||
|
|
||||||
CDNWS_HOST = "cdnws.api.huya.com"
|
CDNWS_HOST = "cdnws.api.huya.com"
|
||||||
LIVELINK_BIND_API_BASE = "https://livelinkbind.game.qq.com"
|
LIVELINK_BIND_API_BASE = "https://livelinkbind.game.qq.com"
|
||||||
PC_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
PC_UA = (
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||||
"Chrome/149.0.0.0 Safari/537.36")
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/149.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
HTTP_HUYA_UA = "webh5&0.0.1&websocket&&diypc_52775"
|
HTTP_HUYA_UA = "webh5&0.0.1&websocket&&diypc_52775"
|
||||||
|
|
||||||
|
|
||||||
@@ -42,6 +45,7 @@ class WSConnectParaInfo(TafStruct):
|
|||||||
tag9 sTraceId string "hex8:hex8:0:0"
|
tag9 sTraceId string "hex8:hex8:0:0"
|
||||||
tag10 mCustomHeaders Map<string,string>
|
tag10 mCustomHeaders Map<string,string>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.lUid: int = 0
|
self.lUid: int = 0
|
||||||
self.sGuid: str = ""
|
self.sGuid: str = ""
|
||||||
@@ -88,12 +92,13 @@ class WSConnectParaInfo(TafStruct):
|
|||||||
|
|
||||||
def _gen_trace_id() -> str:
|
def _gen_trace_id() -> str:
|
||||||
"""生成 sTraceId (格式 hex8:hex8:0:0,HAR 实证)"""
|
"""生成 sTraceId (格式 hex8:hex8:0:0,HAR 实证)"""
|
||||||
h = '%016x' % random.getrandbits(64)
|
h = "%016x" % random.getrandbits(64)
|
||||||
return f"{h}:{h}:0:0"
|
return f"{h}:{h}:0:0"
|
||||||
|
|
||||||
|
|
||||||
def generate_http_baseinfo(uid: int, guid: str, cookie: str,
|
def generate_http_baseinfo(
|
||||||
trace_id: str = None) -> str:
|
uid: int, guid: str, cookie: str, trace_id: str | None = None
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
构造 HTTP POST 的 baseinfo URL 参数
|
构造 HTTP POST 的 baseinfo URL 参数
|
||||||
|
|
||||||
@@ -111,14 +116,14 @@ def generate_http_baseinfo(uid: int, guid: str, cookie: str,
|
|||||||
info.write_to(os)
|
info.write_to(os)
|
||||||
raw = os.get_bytes()
|
raw = os.get_bytes()
|
||||||
# base64 (与 JS window.btoa 一致)
|
# base64 (与 JS window.btoa 一致)
|
||||||
b64 = base64.b64encode(raw).decode('ascii')
|
b64 = base64.b64encode(raw).decode("ascii")
|
||||||
return urllib.parse.quote(b64, safe='')
|
return urllib.parse.quote(b64, safe="")
|
||||||
|
|
||||||
|
|
||||||
class HuyaHttpClient:
|
class HuyaHttpClient:
|
||||||
"""虎牙 HTTP POST RPC 客户端"""
|
"""虎牙 HTTP POST RPC 客户端"""
|
||||||
|
|
||||||
def __init__(self, logger: Callable[[str], None] = None):
|
def __init__(self, logger: Callable[[str], None] | None = None):
|
||||||
self.logger = logger or print
|
self.logger = logger or print
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -155,7 +160,11 @@ class HuyaHttpClient:
|
|||||||
"""HTTP 业务 UserId.sCookie 需要带 huya_ua 前缀,避免重复写入。"""
|
"""HTTP 业务 UserId.sCookie 需要带 huya_ua 前缀,避免重复写入。"""
|
||||||
if not cookie:
|
if not cookie:
|
||||||
return ""
|
return ""
|
||||||
pairs = [(key, value) for key, value in cookie_pairs(normalize_huya_cookie(cookie)) if key != "huya_ua"]
|
pairs = [
|
||||||
|
(key, value)
|
||||||
|
for key, value in cookie_pairs(normalize_huya_cookie(cookie))
|
||||||
|
if key != "huya_ua"
|
||||||
|
]
|
||||||
if not any(key == "guid" for key, _ in pairs):
|
if not any(key == "guid" for key, _ in pairs):
|
||||||
guid = cls._resolve_cookie_guid(cookie)
|
guid = cls._resolve_cookie_guid(cookie)
|
||||||
if guid:
|
if guid:
|
||||||
@@ -168,7 +177,7 @@ class HuyaHttpClient:
|
|||||||
uid: int,
|
uid: int,
|
||||||
guid: str,
|
guid: str,
|
||||||
cookie: str,
|
cookie: str,
|
||||||
trace_id: str = None,
|
trace_id: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""生成与浏览器 cdnws 调用一致的 baseinfo。"""
|
"""生成与浏览器 cdnws 调用一致的 baseinfo。"""
|
||||||
rpc_cookie = cls._normalize_cookie(cookie)
|
rpc_cookie = cls._normalize_cookie(cookie)
|
||||||
@@ -179,6 +188,7 @@ class HuyaHttpClient:
|
|||||||
def _build_user(uid: int, guid: str, cookie: str):
|
def _build_user(uid: int, guid: str, cookie: str):
|
||||||
"""构造 HTTP 业务 UserId。"""
|
"""构造 HTTP 业务 UserId。"""
|
||||||
from .shop_structs import UserId
|
from .shop_structs import UserId
|
||||||
|
|
||||||
user = UserId()
|
user = UserId()
|
||||||
user.lUid = uid
|
user.lUid = uid
|
||||||
user.sGuid = guid or HuyaHttpClient._resolve_cookie_guid(cookie)
|
user.sGuid = guid or HuyaHttpClient._resolve_cookie_guid(cookie)
|
||||||
@@ -194,6 +204,7 @@ class HuyaHttpClient:
|
|||||||
def _build_shop_app(source_id: str = "yellowcarlist", scene: int = 7):
|
def _build_shop_app(source_id: str = "yellowcarlist", scene: int = 7):
|
||||||
"""构造 HTTP 业务 ShopAppInfo。"""
|
"""构造 HTTP 业务 ShopAppInfo。"""
|
||||||
from .shop_structs import ShopAppInfo
|
from .shop_structs import ShopAppInfo
|
||||||
|
|
||||||
info = ShopAppInfo()
|
info = ShopAppInfo()
|
||||||
info.sAppId = "huya"
|
info.sAppId = "huya"
|
||||||
info.sBizType = ""
|
info.sBizType = ""
|
||||||
@@ -205,6 +216,7 @@ class HuyaHttpClient:
|
|||||||
def _build_order_env(item_count: int = 1):
|
def _build_order_env(item_count: int = 1):
|
||||||
"""构造下单/支付环境参数。"""
|
"""构造下单/支付环境参数。"""
|
||||||
import time
|
import time
|
||||||
|
|
||||||
click_time = int(time.time() * 1000)
|
click_time = int(time.time() * 1000)
|
||||||
return {
|
return {
|
||||||
"user_click_seq": f'[{{"x":258,"y":487,"time":{click_time},"id":""}}]',
|
"user_click_seq": f'[{{"x":258,"y":487,"time":{click_time},"id":""}}]',
|
||||||
@@ -215,16 +227,24 @@ class HuyaHttpClient:
|
|||||||
def _build_activity_user(uid: int, cookie: str):
|
def _build_activity_user(uid: int, cookie: str):
|
||||||
"""构造活动组件 UserId,与当前积分兑换组件保持一致。"""
|
"""构造活动组件 UserId,与当前积分兑换组件保持一致。"""
|
||||||
from .activity_structs import ActivityUserId
|
from .activity_structs import ActivityUserId
|
||||||
|
|
||||||
user = ActivityUserId()
|
user = ActivityUserId()
|
||||||
user.lUid = uid
|
user.lUid = uid
|
||||||
user.sHuYaUA = HTTP_HUYA_UA
|
user.sHuYaUA = HTTP_HUYA_UA
|
||||||
user.sCookie = HuyaHttpClient._normalize_cookie(cookie)
|
user.sCookie = HuyaHttpClient._normalize_cookie(cookie)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def call_rpc(self, service: str, method: str,
|
def call_rpc(
|
||||||
req_struct: TafStruct, rsp_class=None,
|
self,
|
||||||
uid: int = 0, guid: str = "", cookie: str = "",
|
service: str,
|
||||||
timeout: float = 15.0):
|
method: str,
|
||||||
|
req_struct: TafStruct,
|
||||||
|
rsp_class=None,
|
||||||
|
uid: int = 0,
|
||||||
|
guid: str = "",
|
||||||
|
cookie: str = "",
|
||||||
|
timeout: float = 15.0,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
HTTP POST RPC 调用
|
HTTP POST RPC 调用
|
||||||
|
|
||||||
@@ -249,28 +269,38 @@ class HuyaHttpClient:
|
|||||||
|
|
||||||
self.logger(f"[HTTP] POST {service}.{method}")
|
self.logger(f"[HTTP] POST {service}.{method}")
|
||||||
self.logger(f"[HTTP] URL: https://{CDNWS_HOST}/?baseinfo=<redacted>")
|
self.logger(f"[HTTP] URL: https://{CDNWS_HOST}/?baseinfo=<redacted>")
|
||||||
self.logger(f"[HTTP] 发送 body: {len(wup_data)} 字节, hex前60: {wup_data[:60].hex()}")
|
self.logger(
|
||||||
|
f"[HTTP] 发送 body: {len(wup_data)} 字节, hex前60: {wup_data[:60].hex()}"
|
||||||
|
)
|
||||||
|
|
||||||
# 3. POST
|
# 3. POST
|
||||||
req = urllib.request.Request(url, data=wup_data, method='POST', headers={
|
req = urllib.request.Request(
|
||||||
'User-Agent': PC_UA,
|
url,
|
||||||
'Origin': 'https://zt.huya.com',
|
data=wup_data,
|
||||||
'Referer': 'https://zt.huya.com/',
|
method="POST",
|
||||||
'Content-Type': 'application/octet-stream',
|
headers={
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
"User-Agent": PC_UA,
|
||||||
})
|
"Origin": "https://zt.huya.com",
|
||||||
|
"Referer": "https://zt.huya.com/",
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
|
},
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
resp_data = resp.read()
|
resp_data = resp.read()
|
||||||
# 检测 gzip 压缩 (响应 hex 1f8b 开头)
|
# 检测 gzip 压缩 (响应 hex 1f8b 开头)
|
||||||
if resp_data[:2] == b'\x1f\x8b':
|
if resp_data[:2] == b"\x1f\x8b":
|
||||||
import gzip
|
import gzip
|
||||||
|
|
||||||
resp_data = gzip.decompress(resp_data)
|
resp_data = gzip.decompress(resp_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger(f"[HTTP] ❌ 请求失败: {type(e).__name__}: {e}")
|
self.logger(f"[HTTP] ❌ 请求失败: {type(e).__name__}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self.logger(f"[HTTP] 收到响应: {len(resp_data)} 字节, hex前60: {resp_data[:60].hex()}")
|
self.logger(
|
||||||
|
f"[HTTP] 收到响应: {len(resp_data)} 字节, hex前60: {resp_data[:60].hex()}"
|
||||||
|
)
|
||||||
|
|
||||||
# 4. 解析响应 Wup
|
# 4. 解析响应 Wup
|
||||||
wup_resp = WupResponse()
|
wup_resp = WupResponse()
|
||||||
@@ -289,7 +319,12 @@ class HuyaHttpClient:
|
|||||||
if result is None:
|
if result is None:
|
||||||
result = wup_resp.readStruct("tResp", rsp_class)
|
result = wup_resp.readStruct("tResp", rsp_class)
|
||||||
if result is not None and hasattr(result, "to_dict"):
|
if result is not None and hasattr(result, "to_dict"):
|
||||||
log_data = result.to_log_dict() if hasattr(result, "to_log_dict") else result.to_dict()
|
decoded_result: Any = result
|
||||||
|
log_data = (
|
||||||
|
decoded_result.to_log_dict()
|
||||||
|
if hasattr(decoded_result, "to_log_dict")
|
||||||
|
else decoded_result.to_dict()
|
||||||
|
)
|
||||||
decoded = json.dumps(log_data, ensure_ascii=False, separators=(",", ":"))
|
decoded = json.dumps(log_data, ensure_ascii=False, separators=(",", ":"))
|
||||||
self.logger(f"[HTTP] 解码 tRsp: {decoded}")
|
self.logger(f"[HTTP] 解码 tRsp: {decoded}")
|
||||||
return result
|
return result
|
||||||
@@ -299,6 +334,7 @@ class HuyaHttpClient:
|
|||||||
def query_user_score(self, uid: int, cookie: str, sid: int, timeout: float = 15.0):
|
def query_user_score(self, uid: int, cookie: str, sid: int, timeout: float = 15.0):
|
||||||
"""查询活动可用积分。"""
|
"""查询活动可用积分。"""
|
||||||
from .activity_structs import GetUserScoreReq, GetUserScoreResp
|
from .activity_structs import GetUserScoreReq, GetUserScoreResp
|
||||||
|
|
||||||
req = GetUserScoreReq()
|
req = GetUserScoreReq()
|
||||||
req.userId = self._build_activity_user(uid, cookie)
|
req.userId = self._build_activity_user(uid, cookie)
|
||||||
req.sid = sid
|
req.sid = sid
|
||||||
@@ -312,9 +348,12 @@ class HuyaHttpClient:
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_act_prize_list(self, uid: int, cookie: str, sid: int, timeout: float = 15.0):
|
def get_act_prize_list(
|
||||||
|
self, uid: int, cookie: str, sid: int, timeout: float = 15.0
|
||||||
|
):
|
||||||
"""查询活动可兑换商品列表。"""
|
"""查询活动可兑换商品列表。"""
|
||||||
from .activity_structs import GetActPrizeListReq, GetActPrizeListResp
|
from .activity_structs import GetActPrizeListReq, GetActPrizeListResp
|
||||||
|
|
||||||
req = GetActPrizeListReq()
|
req = GetActPrizeListReq()
|
||||||
req.userId = self._build_activity_user(uid, cookie)
|
req.userId = self._build_activity_user(uid, cookie)
|
||||||
req.sid = sid
|
req.sid = sid
|
||||||
@@ -328,9 +367,12 @@ class HuyaHttpClient:
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_user_prize_records(self, uid: int, cookie: str, sid: int, timeout: float = 15.0):
|
def get_user_prize_records(
|
||||||
|
self, uid: int, cookie: str, sid: int, timeout: float = 15.0
|
||||||
|
):
|
||||||
"""查询用户兑换记录。"""
|
"""查询用户兑换记录。"""
|
||||||
from .activity_structs import GetUserPrizeRecordsReq, GetUserPrizeRecordsResp
|
from .activity_structs import GetUserPrizeRecordsReq, GetUserPrizeRecordsResp
|
||||||
|
|
||||||
req = GetUserPrizeRecordsReq()
|
req = GetUserPrizeRecordsReq()
|
||||||
req.userId = self._build_activity_user(uid, cookie)
|
req.userId = self._build_activity_user(uid, cookie)
|
||||||
req.sid = sid
|
req.sid = sid
|
||||||
@@ -344,9 +386,12 @@ class HuyaHttpClient:
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
def score_exchange_prize(self, uid: int, cookie: str, sid: int, pid: int, timeout: float = 15.0):
|
def score_exchange_prize(
|
||||||
|
self, uid: int, cookie: str, sid: int, pid: int, timeout: float = 15.0
|
||||||
|
):
|
||||||
"""兑换活动积分商品。"""
|
"""兑换活动积分商品。"""
|
||||||
from .activity_structs import ScoreExchangePrizeReq, ScoreExchangePrizeResp
|
from .activity_structs import ScoreExchangePrizeReq, ScoreExchangePrizeResp
|
||||||
|
|
||||||
req = ScoreExchangePrizeReq()
|
req = ScoreExchangePrizeReq()
|
||||||
req.userId = self._build_activity_user(uid, cookie)
|
req.userId = self._build_activity_user(uid, cookie)
|
||||||
req.sid = int(sid or 0)
|
req.sid = int(sid or 0)
|
||||||
@@ -361,9 +406,12 @@ class HuyaHttpClient:
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_act_task_detail(self, uid: int, cookie: str, act_id: int, timeout: float = 15.0):
|
def get_act_task_detail(
|
||||||
|
self, uid: int, cookie: str, act_id: int, timeout: float = 15.0
|
||||||
|
):
|
||||||
"""查询活动任务详情,用于发现充值商品 SPU。"""
|
"""查询活动任务详情,用于发现充值商品 SPU。"""
|
||||||
from .activity_structs import GetActTaskDetailReq, GetActTaskDetailResp
|
from .activity_structs import GetActTaskDetailReq, GetActTaskDetailResp
|
||||||
|
|
||||||
req = GetActTaskDetailReq()
|
req = GetActTaskDetailReq()
|
||||||
req.userId = self._build_activity_user(uid, cookie)
|
req.userId = self._build_activity_user(uid, cookie)
|
||||||
req.actId = int(act_id or 0)
|
req.actId = int(act_id or 0)
|
||||||
@@ -389,7 +437,11 @@ class HuyaHttpClient:
|
|||||||
timeout: float = 15.0,
|
timeout: float = 15.0,
|
||||||
):
|
):
|
||||||
"""查询用户游戏账号绑定状态。"""
|
"""查询用户游戏账号绑定状态。"""
|
||||||
from .activity_structs import CheckUserBindGameAccountReq, CheckUserBindGameAccountResp
|
from .activity_structs import (
|
||||||
|
CheckUserBindGameAccountReq,
|
||||||
|
CheckUserBindGameAccountResp,
|
||||||
|
)
|
||||||
|
|
||||||
req = CheckUserBindGameAccountReq()
|
req = CheckUserBindGameAccountReq()
|
||||||
req.userId = self._build_activity_user(uid, cookie)
|
req.userId = self._build_activity_user(uid, cookie)
|
||||||
req.gid = int(gid or 0)
|
req.gid = int(gid or 0)
|
||||||
@@ -418,7 +470,11 @@ class HuyaHttpClient:
|
|||||||
timeout: float = 15.0,
|
timeout: float = 15.0,
|
||||||
):
|
):
|
||||||
"""确认虎牙号与当前活动的游戏账号绑定关系。"""
|
"""确认虎牙号与当前活动的游戏账号绑定关系。"""
|
||||||
from .activity_structs import ConfirmBindActAccountReq, ConfirmBindActAccountResp
|
from .activity_structs import (
|
||||||
|
ConfirmBindActAccountReq,
|
||||||
|
ConfirmBindActAccountResp,
|
||||||
|
)
|
||||||
|
|
||||||
req = ConfirmBindActAccountReq()
|
req = ConfirmBindActAccountReq()
|
||||||
req.userId = self._build_activity_user(uid, cookie)
|
req.userId = self._build_activity_user(uid, cookie)
|
||||||
req.gid = int(gid or 0)
|
req.gid = int(gid or 0)
|
||||||
@@ -446,6 +502,7 @@ class HuyaHttpClient:
|
|||||||
):
|
):
|
||||||
"""获取绑定二维码拉起参数。"""
|
"""获取绑定二维码拉起参数。"""
|
||||||
from .activity_structs import GetLiveLinkParamReq, GetLiveLinkParamResp
|
from .activity_structs import GetLiveLinkParamReq, GetLiveLinkParamResp
|
||||||
|
|
||||||
req = GetLiveLinkParamReq()
|
req = GetLiveLinkParamReq()
|
||||||
req.userId = self._build_activity_user(uid, cookie)
|
req.userId = self._build_activity_user(uid, cookie)
|
||||||
req.gid = int(gid or 0)
|
req.gid = int(gid or 0)
|
||||||
@@ -463,9 +520,12 @@ class HuyaHttpClient:
|
|||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_user_profile_batch(self, uid: int, cookie: str, target_uids: list[int], timeout: float = 15.0):
|
def get_user_profile_batch(
|
||||||
|
self, uid: int, cookie: str, target_uids: list[int], timeout: float = 15.0
|
||||||
|
):
|
||||||
"""批量查询虎牙用户资料,用于补齐绑定页头像和昵称。"""
|
"""批量查询虎牙用户资料,用于补齐绑定页头像和昵称。"""
|
||||||
from .activity_structs import GetUserProfileBatchReq, GetUserProfileBatchResp
|
from .activity_structs import GetUserProfileBatchReq, GetUserProfileBatchResp
|
||||||
|
|
||||||
req = GetUserProfileBatchReq()
|
req = GetUserProfileBatchReq()
|
||||||
req.userId = self._build_activity_user(0, "")
|
req.userId = self._build_activity_user(0, "")
|
||||||
req.uidList = [int(item) for item in target_uids if int(item or 0)]
|
req.uidList = [int(item) for item in target_uids if int(item or 0)]
|
||||||
@@ -493,7 +553,9 @@ class HuyaHttpClient:
|
|||||||
mime = "image/png" if text.startswith("iVBOR") else "image/jpeg"
|
mime = "image/png" if text.startswith("iVBOR") else "image/jpeg"
|
||||||
return f"data:{mime};base64,{text}"
|
return f"data:{mime};base64,{text}"
|
||||||
|
|
||||||
def get_livelink_mini_qrcode(self, bind_page_url: str, timeout: float = 15.0) -> dict | None:
|
def get_livelink_mini_qrcode(
|
||||||
|
self, bind_page_url: str, timeout: float = 15.0
|
||||||
|
) -> dict | None:
|
||||||
"""请求绑定页内部接口,获取真正可扫的小程序码图片。"""
|
"""请求绑定页内部接口,获取真正可扫的小程序码图片。"""
|
||||||
import gzip
|
import gzip
|
||||||
import time
|
import time
|
||||||
@@ -522,24 +584,32 @@ class HuyaHttpClient:
|
|||||||
self.logger(
|
self.logger(
|
||||||
f"[LIVELINK] POST getMiniAppQrcode plat={live_plat_id or '-'} actId={act_id or '-'}"
|
f"[LIVELINK] POST getMiniAppQrcode plat={live_plat_id or '-'} actId={act_id or '-'}"
|
||||||
)
|
)
|
||||||
req = urllib.request.Request(endpoint, data=payload, method="POST", headers={
|
req = urllib.request.Request(
|
||||||
"User-Agent": PC_UA,
|
endpoint,
|
||||||
"Origin": "https://livelink.qq.com",
|
data=payload,
|
||||||
"Referer": "https://livelink.qq.com/",
|
method="POST",
|
||||||
"Content-Type": "application/json;charset=UTF-8",
|
headers={
|
||||||
"Accept": "application/json, text/plain, */*",
|
"User-Agent": PC_UA,
|
||||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
"Origin": "https://livelink.qq.com",
|
||||||
"Gap-Plat-Id": live_plat_id,
|
"Referer": "https://livelink.qq.com/",
|
||||||
"Gap-Act-Id": act_id,
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
"Gap-User-From": "pc",
|
"Accept": "application/json, text/plain, */*",
|
||||||
"Gap-From-Id": from_id,
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
"Vulcan-AccType": "auth_in_h5",
|
"Gap-Plat-Id": live_plat_id,
|
||||||
"Vulcan-PlatUser-Session": "",
|
"Gap-Act-Id": act_id,
|
||||||
})
|
"Gap-User-From": "pc",
|
||||||
|
"Gap-From-Id": from_id,
|
||||||
|
"Vulcan-AccType": "auth_in_h5",
|
||||||
|
"Vulcan-PlatUser-Session": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
resp_data = resp.read()
|
resp_data = resp.read()
|
||||||
if resp_data[:2] == b"\x1f\x8b" or resp.headers.get("Content-Encoding") == "gzip":
|
if (
|
||||||
|
resp_data[:2] == b"\x1f\x8b"
|
||||||
|
or resp.headers.get("Content-Encoding") == "gzip"
|
||||||
|
):
|
||||||
resp_data = gzip.decompress(resp_data)
|
resp_data = gzip.decompress(resp_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger(f"[LIVELINK] ❌ 小程序码请求失败: {type(e).__name__}: {e}")
|
self.logger(f"[LIVELINK] ❌ 小程序码请求失败: {type(e).__name__}: {e}")
|
||||||
@@ -552,7 +622,9 @@ class HuyaHttpClient:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if data.get("iRet") != 0:
|
if data.get("iRet") != 0:
|
||||||
self.logger(f"[LIVELINK] ❌ 小程序码接口失败: iRet={data.get('iRet')} msg={data.get('sMsg') or '-'}")
|
self.logger(
|
||||||
|
f"[LIVELINK] ❌ 小程序码接口失败: iRet={data.get('iRet')} msg={data.get('sMsg') or '-'}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
jdata = data.get("jData") if isinstance(data.get("jData"), dict) else {}
|
jdata = data.get("jData") if isinstance(data.get("jData"), dict) else {}
|
||||||
@@ -562,13 +634,17 @@ class HuyaHttpClient:
|
|||||||
self.logger("[LIVELINK] ❌ 小程序码接口未返回 qrcode")
|
self.logger("[LIVELINK] ❌ 小程序码接口未返回 qrcode")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self.logger(f"[LIVELINK] 小程序码获取成功: image={len(image_src)} 字符 token={'有' if jdata.get('qrcodeToken') else '无'}")
|
self.logger(
|
||||||
|
f"[LIVELINK] 小程序码获取成功: image={len(image_src)} 字符 token={'有' if jdata.get('qrcodeToken') else '无'}"
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"mini_qrcode_image": image_src,
|
"mini_qrcode_image": image_src,
|
||||||
"qrcode_token": jdata.get("qrcodeToken") or "",
|
"qrcode_token": jdata.get("qrcodeToken") or "",
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_livelink_qrcode_status(self, qrcode_token: str, timeout: float = 15.0) -> dict | None:
|
def get_livelink_qrcode_status(
|
||||||
|
self, qrcode_token: str, timeout: float = 15.0
|
||||||
|
) -> dict | None:
|
||||||
"""轮询 livelink 小程序码扫码状态。"""
|
"""轮询 livelink 小程序码扫码状态。"""
|
||||||
import gzip
|
import gzip
|
||||||
|
|
||||||
@@ -582,18 +658,26 @@ class HuyaHttpClient:
|
|||||||
separators=(",", ":"),
|
separators=(",", ":"),
|
||||||
).encode("utf-8")
|
).encode("utf-8")
|
||||||
endpoint = f"{LIVELINK_BIND_API_BASE}/api/qrcode/poolingQrcodeStatus"
|
endpoint = f"{LIVELINK_BIND_API_BASE}/api/qrcode/poolingQrcodeStatus"
|
||||||
req = urllib.request.Request(endpoint, data=payload, method="POST", headers={
|
req = urllib.request.Request(
|
||||||
"User-Agent": PC_UA,
|
endpoint,
|
||||||
"Origin": "https://livelink.qq.com",
|
data=payload,
|
||||||
"Referer": "https://livelink.qq.com/",
|
method="POST",
|
||||||
"Content-Type": "application/json;charset=UTF-8",
|
headers={
|
||||||
"Accept": "application/json, text/plain, */*",
|
"User-Agent": PC_UA,
|
||||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
"Origin": "https://livelink.qq.com",
|
||||||
})
|
"Referer": "https://livelink.qq.com/",
|
||||||
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
|
"Accept": "application/json, text/plain, */*",
|
||||||
|
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||||
|
},
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
resp_data = resp.read()
|
resp_data = resp.read()
|
||||||
if resp_data[:2] == b"\x1f\x8b" or resp.headers.get("Content-Encoding") == "gzip":
|
if (
|
||||||
|
resp_data[:2] == b"\x1f\x8b"
|
||||||
|
or resp.headers.get("Content-Encoding") == "gzip"
|
||||||
|
):
|
||||||
resp_data = gzip.decompress(resp_data)
|
resp_data = gzip.decompress(resp_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger(f"[LIVELINK] ❌ 二维码状态请求失败: {type(e).__name__}: {e}")
|
self.logger(f"[LIVELINK] ❌ 二维码状态请求失败: {type(e).__name__}: {e}")
|
||||||
@@ -602,11 +686,15 @@ class HuyaHttpClient:
|
|||||||
try:
|
try:
|
||||||
data = json.loads(resp_data.decode("utf-8", "replace"))
|
data = json.loads(resp_data.decode("utf-8", "replace"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger(f"[LIVELINK] ❌ 二维码状态响应解析失败: {type(e).__name__}: {e}")
|
self.logger(
|
||||||
|
f"[LIVELINK] ❌ 二维码状态响应解析失败: {type(e).__name__}: {e}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if data.get("iRet") != 0:
|
if data.get("iRet") != 0:
|
||||||
self.logger(f"[LIVELINK] ❌ 二维码状态接口失败: iRet={data.get('iRet')} msg={data.get('sMsg') or '-'}")
|
self.logger(
|
||||||
|
f"[LIVELINK] ❌ 二维码状态接口失败: iRet={data.get('iRet')} msg={data.get('sMsg') or '-'}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
jdata = data.get("jData") if isinstance(data.get("jData"), dict) else {}
|
jdata = data.get("jData") if isinstance(data.get("jData"), dict) else {}
|
||||||
@@ -675,9 +763,20 @@ class HuyaHttpClient:
|
|||||||
"exchange_qq_coin_url": f"{huya_base}?{urlencode(exchange_params)}",
|
"exchange_qq_coin_url": f"{huya_base}?{urlencode(exchange_params)}",
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_goods_info(self, uid, guid, cookie, pid, spu_id, sku_id=0, game_id="",
|
def get_goods_info(
|
||||||
source_id="yellowcarlist", scene=7):
|
self,
|
||||||
|
uid,
|
||||||
|
guid,
|
||||||
|
cookie,
|
||||||
|
pid,
|
||||||
|
spu_id,
|
||||||
|
sku_id=0,
|
||||||
|
game_id="",
|
||||||
|
source_id="yellowcarlist",
|
||||||
|
scene=7,
|
||||||
|
):
|
||||||
from .shop_structs import GetGoodsInfoReqV5, GoodsInfoRsp
|
from .shop_structs import GetGoodsInfoReqV5, GoodsInfoRsp
|
||||||
|
|
||||||
req = GetGoodsInfoReqV5()
|
req = GetGoodsInfoReqV5()
|
||||||
req.userId = self._build_user(uid, guid, cookie)
|
req.userId = self._build_user(uid, guid, cookie)
|
||||||
req.shopAppInfo = self._build_shop_app(source_id, scene)
|
req.shopAppInfo = self._build_shop_app(source_id, scene)
|
||||||
@@ -685,11 +784,21 @@ class HuyaHttpClient:
|
|||||||
req.spuId = spu_id
|
req.spuId = spu_id
|
||||||
req.skuId = sku_id
|
req.skuId = sku_id
|
||||||
req.gameId = game_id
|
req.gameId = game_id
|
||||||
return self.call_rpc("shopMiddleUI", "getGoodsInfoV5", req,
|
return self.call_rpc(
|
||||||
GoodsInfoRsp, uid, guid, cookie)
|
"shopMiddleUI", "getGoodsInfoV5", req, GoodsInfoRsp, uid, guid, cookie
|
||||||
|
)
|
||||||
|
|
||||||
def query_user_order_list(self, uid, guid, cookie, offset=0, page_size=10,
|
def query_user_order_list(
|
||||||
order_type=1, status=0, timeout=15.0):
|
self,
|
||||||
|
uid,
|
||||||
|
guid,
|
||||||
|
cookie,
|
||||||
|
offset=0,
|
||||||
|
page_size=10,
|
||||||
|
order_type=1,
|
||||||
|
status=0,
|
||||||
|
timeout=15.0,
|
||||||
|
):
|
||||||
"""查询用户订单列表,用于支付二维码后的付款状态轮询。"""
|
"""查询用户订单列表,用于支付二维码后的付款状态轮询。"""
|
||||||
from .shop_structs import QueryUserOrderListReq, QueryUserOrderListRsp
|
from .shop_structs import QueryUserOrderListReq, QueryUserOrderListRsp
|
||||||
|
|
||||||
@@ -724,13 +833,28 @@ class HuyaHttpClient:
|
|||||||
)
|
)
|
||||||
return fallback if fallback is not None else result
|
return fallback if fallback is not None else result
|
||||||
|
|
||||||
def create_order(self, uid, guid, cookie, pid, spu_id, sku_id,
|
def create_order(
|
||||||
item_count=1, source_id="yellowcarlist", game_id="", scene=7,
|
self,
|
||||||
order_type=None):
|
uid,
|
||||||
|
guid,
|
||||||
|
cookie,
|
||||||
|
pid,
|
||||||
|
spu_id,
|
||||||
|
sku_id,
|
||||||
|
item_count=1,
|
||||||
|
source_id="yellowcarlist",
|
||||||
|
game_id="",
|
||||||
|
scene=7,
|
||||||
|
order_type=None,
|
||||||
|
):
|
||||||
"""order_type=None 时自动尝试从 1 到 10 找到有效值"""
|
"""order_type=None 时自动尝试从 1 到 10 找到有效值"""
|
||||||
from .shop_structs import (CreateOrderReqV5, CreateOrderRsp,
|
from .shop_structs import (
|
||||||
CreateOrderExtraParam, CreateOrderPromotionParam,
|
CreateOrderReqV5,
|
||||||
CreateOrderAccountParam)
|
CreateOrderRsp,
|
||||||
|
CreateOrderExtraParam,
|
||||||
|
CreateOrderPromotionParam,
|
||||||
|
CreateOrderAccountParam,
|
||||||
|
)
|
||||||
|
|
||||||
# 如果指定了具体值,直接试
|
# 如果指定了具体值,直接试
|
||||||
types_to_try = [order_type] if order_type is not None else list(range(1, 11))
|
types_to_try = [order_type] if order_type is not None else list(range(1, 11))
|
||||||
@@ -759,10 +883,13 @@ class HuyaHttpClient:
|
|||||||
req.gameCategoryId = 507
|
req.gameCategoryId = 507
|
||||||
req.ext = ""
|
req.ext = ""
|
||||||
|
|
||||||
result = self.call_rpc("shopMiddleUI", "createOrderV5", req,
|
result: Any = self.call_rpc(
|
||||||
CreateOrderRsp, uid, guid, cookie)
|
"shopMiddleUI", "createOrderV5", req, CreateOrderRsp, uid, guid, cookie
|
||||||
|
)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
self.logger(f" orderType={ot}: code={result.code} orderId={result.orderId} msg={result.message}")
|
self.logger(
|
||||||
|
f" orderType={ot}: code={result.code} orderId={result.orderId} msg={result.message}"
|
||||||
|
)
|
||||||
if result.code == 200 and result.orderId:
|
if result.code == 200 and result.orderId:
|
||||||
self.logger(f"[✓] 找到有效 orderType={ot}")
|
self.logger(f"[✓] 找到有效 orderType={ot}")
|
||||||
return result
|
return result
|
||||||
@@ -781,11 +908,21 @@ class HuyaHttpClient:
|
|||||||
return "Weixin"
|
return "Weixin"
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def pay_order_submit(self, uid, guid, cookie, order_id, pay_type=1,
|
def pay_order_submit(
|
||||||
pid=0, source_id="yellowcarlist", scene=7,
|
self,
|
||||||
item_count=1):
|
uid,
|
||||||
|
guid,
|
||||||
|
cookie,
|
||||||
|
order_id,
|
||||||
|
pay_type=1,
|
||||||
|
pid=0,
|
||||||
|
source_id="yellowcarlist",
|
||||||
|
scene=7,
|
||||||
|
item_count=1,
|
||||||
|
):
|
||||||
"""发起支付,结构与 WSS HAR 中 payOrderSubmitV5 对齐。"""
|
"""发起支付,结构与 WSS HAR 中 payOrderSubmitV5 对齐。"""
|
||||||
from .shop_structs import PayOrderRes
|
from .shop_structs import PayOrderRes
|
||||||
|
|
||||||
os = TafOutputStream()
|
os = TafOutputStream()
|
||||||
os.write_struct_begin(0)
|
os.write_struct_begin(0)
|
||||||
|
|
||||||
@@ -817,22 +954,31 @@ class HuyaHttpClient:
|
|||||||
self.logger(f"[HTTP] POST shopMiddleUI.payOrderSubmitV5")
|
self.logger(f"[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(url, data=wup_data, method='POST', headers={
|
req = urllib.request.Request(
|
||||||
'User-Agent': PC_UA, 'Origin': 'https://zt.huya.com',
|
url,
|
||||||
'Referer': 'https://zt.huya.com/',
|
data=wup_data,
|
||||||
'Content-Type': 'application/octet-stream',
|
method="POST",
|
||||||
})
|
headers={
|
||||||
|
"User-Agent": PC_UA,
|
||||||
|
"Origin": "https://zt.huya.com",
|
||||||
|
"Referer": "https://zt.huya.com/",
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
},
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||||
resp_data = resp.read()
|
resp_data = resp.read()
|
||||||
if resp_data[:2] == b'\x1f\x8b':
|
if resp_data[:2] == b"\x1f\x8b":
|
||||||
import gzip
|
import gzip
|
||||||
|
|
||||||
resp_data = gzip.decompress(resp_data)
|
resp_data = gzip.decompress(resp_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger(f"[HTTP] ❌ payOrderSubmitV5 失败: {e}")
|
self.logger(f"[HTTP] ❌ payOrderSubmitV5 失败: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
self.logger(f"[HTTP] payOrderSubmitV5 响应 {len(resp_data)} 字节, hex前60: {resp_data[:60].hex()}")
|
self.logger(
|
||||||
|
f"[HTTP] payOrderSubmitV5 响应 {len(resp_data)} 字节, hex前60: {resp_data[:60].hex()}"
|
||||||
|
)
|
||||||
wup_resp = WupResponse()
|
wup_resp = WupResponse()
|
||||||
wup_resp.decode(resp_data)
|
wup_resp.decode(resp_data)
|
||||||
if wup_resp.newdata:
|
if wup_resp.newdata:
|
||||||
|
|||||||
+210
-112
@@ -14,13 +14,14 @@
|
|||||||
- 服务端响应 seq ≠ 请求 seq → 必须用 FIFO 匹配
|
- 服务端响应 seq ≠ 请求 seq → 必须用 FIFO 匹配
|
||||||
- 初始化 wsLaunch iRequestId=-1,业务 requestId 从 8 开始递增
|
- 初始化 wsLaunch iRequestId=-1,业务 requestId 从 8 开始递增
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import Optional, Callable
|
from typing import Any, Optional, Callable, cast
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
@@ -41,19 +42,19 @@ WSS_COOKIE_UA = "webh5&0.0.1&websocket&&diypc_52775"
|
|||||||
WSS_SHOP_SCENE = 4
|
WSS_SHOP_SCENE = 4
|
||||||
|
|
||||||
# seq 按消息类型固定 (HAR 实证,跨两次抓包一致)
|
# seq 按消息类型固定 (HAR 实证,跨两次抓包一致)
|
||||||
SEQ_HEARTBEAT = 0x1d00000d
|
SEQ_HEARTBEAT = 0x1D00000D
|
||||||
SEQ_WSLAUNCH = 0x1d000078
|
SEQ_WSLAUNCH = 0x1D000078
|
||||||
SEQ_GETCONFIG = 0x1d000076
|
SEQ_GETCONFIG = 0x1D000076
|
||||||
SEQ_REGISTER = 0x1d00003c
|
SEQ_REGISTER = 0x1D00003C
|
||||||
SEQ_CONFIRM = 0x1d000002
|
SEQ_CONFIRM = 0x1D000002
|
||||||
SEQ_BUSINESS = 0x1d000106 # 业务 RPC (HAR 7.4 实证:业务帧固定使用该 seq)
|
SEQ_BUSINESS = 0x1D000106 # 业务 RPC (HAR 7.4 实证:业务帧固定使用该 seq)
|
||||||
|
|
||||||
|
|
||||||
class WssCommand:
|
class WssCommand:
|
||||||
RPC_REQUEST = 0x03
|
RPC_REQUEST = 0x03
|
||||||
RPC_RESPONSE = 0x04
|
RPC_RESPONSE = 0x04
|
||||||
AUTH = 0x0a
|
AUTH = 0x0A
|
||||||
SERVER_PUSH1 = 0x0b
|
SERVER_PUSH1 = 0x0B
|
||||||
HEARTBEAT_SEND = 0x10
|
HEARTBEAT_SEND = 0x10
|
||||||
HEARTBEAT_RECV = 0x11
|
HEARTBEAT_RECV = 0x11
|
||||||
CONFIRM_SEND = 0x17
|
CONFIRM_SEND = 0x17
|
||||||
@@ -68,15 +69,15 @@ class WssMessage:
|
|||||||
self.body = body
|
self.body = body
|
||||||
|
|
||||||
def encode(self) -> bytes:
|
def encode(self) -> bytes:
|
||||||
header = struct.pack('>BBI', 0x00, self.command, self.sequence)
|
header = struct.pack(">BBI", 0x00, self.command, self.sequence)
|
||||||
return header + self.body
|
return header + self.body
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def decode(cls, data: bytes) -> 'WssMessage':
|
def decode(cls, data: bytes) -> "WssMessage":
|
||||||
if len(data) < 6:
|
if len(data) < 6:
|
||||||
raise ValueError(f"消息太短: {len(data)} bytes")
|
raise ValueError(f"消息太短: {len(data)} bytes")
|
||||||
version, command = struct.unpack('>BB', data[0:2])
|
version, command = struct.unpack(">BB", data[0:2])
|
||||||
sequence = struct.unpack('>I', data[2:6])[0]
|
sequence = struct.unpack(">I", data[2:6])[0]
|
||||||
body = data[6:]
|
body = data[6:]
|
||||||
return cls(command=command, sequence=sequence, body=body)
|
return cls(command=command, sequence=sequence, body=body)
|
||||||
|
|
||||||
@@ -84,9 +85,11 @@ class WssMessage:
|
|||||||
class HuyaWssClient:
|
class HuyaWssClient:
|
||||||
"""虎牙 WSS 客户端 — 商城通道"""
|
"""虎牙 WSS 客户端 — 商城通道"""
|
||||||
|
|
||||||
def __init__(self, baseinfo: str = None, logger: Callable[[str], None] = None):
|
def __init__(
|
||||||
|
self, baseinfo: str | None = None, logger: Callable[[str], None] | None = None
|
||||||
|
):
|
||||||
self.baseinfo = baseinfo or SHOP_BASEINFO
|
self.baseinfo = baseinfo or SHOP_BASEINFO
|
||||||
self.ws = None
|
self.ws: Any = None
|
||||||
self._biz_seq = SEQ_BUSINESS # 业务 RPC 递增用
|
self._biz_seq = SEQ_BUSINESS # 业务 RPC 递增用
|
||||||
self._wup_req_id = 8
|
self._wup_req_id = 8
|
||||||
self.rpc_queue = deque()
|
self.rpc_queue = deque()
|
||||||
@@ -135,7 +138,12 @@ class HuyaWssClient:
|
|||||||
# 大包头里的长度使用原始 WUP 包长度(含 4B 长度前缀),但正文只放裸 WUP body;
|
# 大包头里的长度使用原始 WUP 包长度(含 4B 长度前缀),但正文只放裸 WUP body;
|
||||||
# 浏览器随后用 trace 尾部的前 4 字节补足该长度窗口。
|
# 浏览器随后用 trace 尾部的前 4 字节补足该长度窗口。
|
||||||
packet_len = len(wup_data)
|
packet_len = len(wup_data)
|
||||||
return bytes([packet_len & 0xFF]) + packet_len.to_bytes(4, "big") + wup_body + HuyaWssClient._make_big_tail()
|
return (
|
||||||
|
bytes([packet_len & 0xFF])
|
||||||
|
+ packet_len.to_bytes(4, "big")
|
||||||
|
+ wup_body
|
||||||
|
+ HuyaWssClient._make_big_tail()
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_biz_cookie(cookie: str) -> str:
|
def _normalize_biz_cookie(cookie: str) -> str:
|
||||||
@@ -143,7 +151,12 @@ class HuyaWssClient:
|
|||||||
cookie = (cookie or "").strip()
|
cookie = (cookie or "").strip()
|
||||||
normalized = f"huya_ua={WSS_COOKIE_UA}"
|
normalized = f"huya_ua={WSS_COOKIE_UA}"
|
||||||
if re.search(r"(?:^|;\s*)huya_ua=", cookie):
|
if re.search(r"(?:^|;\s*)huya_ua=", cookie):
|
||||||
return re.sub(r"(^|;\s*)huya_ua=[^;]*", lambda m: f"{m.group(1)}{normalized}", cookie, count=1)
|
return re.sub(
|
||||||
|
r"(^|;\s*)huya_ua=[^;]*",
|
||||||
|
lambda m: f"{m.group(1)}{normalized}",
|
||||||
|
cookie,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
if not cookie:
|
if not cookie:
|
||||||
return normalized
|
return normalized
|
||||||
return f"{normalized}; {cookie}"
|
return f"{normalized}; {cookie}"
|
||||||
@@ -152,6 +165,7 @@ class HuyaWssClient:
|
|||||||
def _build_biz_user(uid: int, cookie: str):
|
def _build_biz_user(uid: int, cookie: str):
|
||||||
"""构造 WSS 业务 UserId(HAR 实证:guid 为空,cookie 不额外加前缀)"""
|
"""构造 WSS 业务 UserId(HAR 实证:guid 为空,cookie 不额外加前缀)"""
|
||||||
from .shop_structs import UserId
|
from .shop_structs import UserId
|
||||||
|
|
||||||
user = UserId()
|
user = UserId()
|
||||||
user.lUid = uid
|
user.lUid = uid
|
||||||
user.sGuid = ""
|
user.sGuid = ""
|
||||||
@@ -167,6 +181,7 @@ class HuyaWssClient:
|
|||||||
def _build_shop_app(source_id: str = "yellowcarlist", scene: int = WSS_SHOP_SCENE):
|
def _build_shop_app(source_id: str = "yellowcarlist", scene: int = WSS_SHOP_SCENE):
|
||||||
"""构造 WSS 业务 ShopAppInfo"""
|
"""构造 WSS 业务 ShopAppInfo"""
|
||||||
from .shop_structs import ShopAppInfo
|
from .shop_structs import ShopAppInfo
|
||||||
|
|
||||||
info = ShopAppInfo()
|
info = ShopAppInfo()
|
||||||
info.sAppId = "huya"
|
info.sAppId = "huya"
|
||||||
info.sBizType = ""
|
info.sBizType = ""
|
||||||
@@ -183,28 +198,33 @@ class HuyaWssClient:
|
|||||||
"input_1": str(item_count),
|
"input_1": str(item_count),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def connect(self, host: str = SHOP_WS_HOST, timeout: float = 15.0,
|
async def connect(
|
||||||
cookie: str = ""):
|
self, host: str = SHOP_WS_HOST, timeout: float = 15.0, cookie: str = ""
|
||||||
|
):
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
url = f"wss://{host}/?baseinfo={quote(self.baseinfo)}"
|
url = f"wss://{host}/?baseinfo={quote(self.baseinfo)}"
|
||||||
self.logger(f"[WSS] 正在连接 {host} ...")
|
self.logger(f"[WSS] 正在连接 {host} ...")
|
||||||
self.logger(f"[WSS] URL: {url[:100]}...")
|
self.logger(f"[WSS] URL: {url[:100]}...")
|
||||||
|
|
||||||
BROWSER_UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
BROWSER_UA = (
|
||||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
||||||
"Chrome/149.0.0.0 Safari/537.36")
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/149.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
headers = {"Accept-Language": "zh-CN,zh;q=0.9"}
|
headers = {"Accept-Language": "zh-CN,zh;q=0.9"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.ws = await asyncio.wait_for(
|
self.ws = await asyncio.wait_for(
|
||||||
websockets.connect(
|
websockets.connect(
|
||||||
url,
|
url,
|
||||||
origin="https://m-shop.yaoguo.com",
|
origin=cast(Any, "https://m-shop.yaoguo.com"),
|
||||||
user_agent_header=BROWSER_UA,
|
user_agent_header=BROWSER_UA,
|
||||||
additional_headers=headers,
|
additional_headers=headers,
|
||||||
open_timeout=timeout,
|
open_timeout=timeout,
|
||||||
),
|
),
|
||||||
timeout=timeout)
|
timeout=timeout,
|
||||||
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
self.logger(f"[WSS] 连接超时({timeout}s)")
|
self.logger(f"[WSS] 连接超时({timeout}s)")
|
||||||
raise
|
raise
|
||||||
@@ -230,11 +250,16 @@ class HuyaWssClient:
|
|||||||
try:
|
try:
|
||||||
async for raw in self.ws:
|
async for raw in self.ws:
|
||||||
try:
|
try:
|
||||||
msg = WssMessage.decode(raw)
|
raw_bytes = raw if isinstance(raw, bytes) else raw.encode()
|
||||||
self.logger(format_wss_log(msg.body, msg.command, msg.sequence, "收"))
|
msg = WssMessage.decode(raw_bytes)
|
||||||
|
self.logger(
|
||||||
|
format_wss_log(msg.body, msg.command, msg.sequence, "收")
|
||||||
|
)
|
||||||
await self._handle_message(msg)
|
await self._handle_message(msg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger(f"[WSS] 解析消息失败: {e} raw_hex={raw[:50].hex()}")
|
self.logger(
|
||||||
|
f"[WSS] 解析消息失败: {e} raw_hex={raw_bytes[:50].hex()}"
|
||||||
|
)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
except websockets.exceptions.ConnectionClosed as e:
|
except websockets.exceptions.ConnectionClosed as e:
|
||||||
@@ -270,14 +295,17 @@ class HuyaWssClient:
|
|||||||
async def send_heartbeat(self):
|
async def send_heartbeat(self):
|
||||||
# body = LIST(tag0,count=1) + 元素{tag0="live:0", tag1=""} + TAIL
|
# body = LIST(tag0,count=1) + 元素{tag0="live:0", tag1=""} + TAIL
|
||||||
os = TafOutputStream()
|
os = TafOutputStream()
|
||||||
os.write_head(0, TafType.LIST) # 09
|
os.write_head(0, TafType.LIST) # 09
|
||||||
os.write_int32(0, 1) # 00 01 (count=1)
|
os.write_int32(0, 1) # 00 01 (count=1)
|
||||||
os.write_string(0, "live:0") # 06 06 live:0
|
os.write_string(0, "live:0") # 06 06 live:0
|
||||||
os.buf.write(b'\x16\x00') # 16 00 (tag1 "")
|
os.buf.write(b"\x16\x00") # 16 00 (tag1 "")
|
||||||
body = os.get_bytes() + TAIL_BYTES
|
body = os.get_bytes() + TAIL_BYTES
|
||||||
msg = WssMessage(command=WssCommand.HEARTBEAT_SEND,
|
msg = WssMessage(
|
||||||
sequence=SEQ_HEARTBEAT, body=body)
|
command=WssCommand.HEARTBEAT_SEND, sequence=SEQ_HEARTBEAT, body=body
|
||||||
self.logger(format_wss_log(body, WssCommand.HEARTBEAT_SEND, SEQ_HEARTBEAT, "发"))
|
)
|
||||||
|
self.logger(
|
||||||
|
format_wss_log(body, WssCommand.HEARTBEAT_SEND, SEQ_HEARTBEAT, "发")
|
||||||
|
)
|
||||||
await self.ws.send(msg.encode())
|
await self.ws.send(msg.encode())
|
||||||
|
|
||||||
async def initialize(self, uid: int, guid: str, cookie: str):
|
async def initialize(self, uid: int, guid: str, cookie: str):
|
||||||
@@ -295,10 +323,14 @@ class HuyaWssClient:
|
|||||||
if launch_rsp is None:
|
if launch_rsp is None:
|
||||||
self.logger("[✗] wsLaunch 无响应,初始化失败")
|
self.logger("[✗] wsLaunch 无响应,初始化失败")
|
||||||
return False
|
return False
|
||||||
self.logger(f"[初始化] wsLaunch OK guid={self._launch_guid} ip={self._launch_ip}")
|
self.logger(
|
||||||
|
f"[初始化] wsLaunch OK guid={self._launch_guid} ip={self._launch_ip}"
|
||||||
|
)
|
||||||
|
|
||||||
config_rsp = await self.call_get_config_shop()
|
config_rsp = await self.call_get_config_shop()
|
||||||
self.logger(f"[初始化] getConfig {'OK' if config_rsp is not None else '无响应(可继续)'}")
|
self.logger(
|
||||||
|
f"[初始化] getConfig {'OK' if config_rsp is not None else '无响应(可继续)'}"
|
||||||
|
)
|
||||||
|
|
||||||
if self._launch_guid:
|
if self._launch_guid:
|
||||||
await self.send_register(self._launch_guid)
|
await self.send_register(self._launch_guid)
|
||||||
@@ -316,11 +348,11 @@ class HuyaWssClient:
|
|||||||
|
|
||||||
os = TafOutputStream()
|
os = TafOutputStream()
|
||||||
os.write_struct_begin(0)
|
os.write_struct_begin(0)
|
||||||
os.write_int64(0, 0) # lUid = 0
|
os.write_int64(0, 0) # lUid = 0
|
||||||
os.write_string(1, "") # sGuid = ""
|
os.write_string(1, "") # sGuid = ""
|
||||||
os.write_string(2, ua) # sUA
|
os.write_string(2, ua) # sUA
|
||||||
os.write_string(3, app_src) # sAppSrc
|
os.write_string(3, app_src) # sAppSrc
|
||||||
os.write_struct_begin(4) # tDeviceInfo
|
os.write_struct_begin(4) # tDeviceInfo
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
os.write_string(i, "")
|
os.write_string(i, "")
|
||||||
os.write_struct_end()
|
os.write_struct_end()
|
||||||
@@ -336,8 +368,7 @@ class HuyaWssClient:
|
|||||||
body = self._encode_rpc_body(wup_data)
|
body = self._encode_rpc_body(wup_data)
|
||||||
|
|
||||||
seq = SEQ_WSLAUNCH
|
seq = SEQ_WSLAUNCH
|
||||||
msg = WssMessage(command=WssCommand.RPC_REQUEST,
|
msg = WssMessage(command=WssCommand.RPC_REQUEST, sequence=seq, body=body)
|
||||||
sequence=seq, body=body)
|
|
||||||
future = asyncio.Future()
|
future = asyncio.Future()
|
||||||
self.rpc_queue.append(future)
|
self.rpc_queue.append(future)
|
||||||
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "发"))
|
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "发"))
|
||||||
@@ -363,11 +394,13 @@ class HuyaWssClient:
|
|||||||
wup_resp.decode(wup)
|
wup_resp.decode(wup)
|
||||||
treq = wup_resp.newdata.get("tRsp") or wup_resp.newdata.get("tResp")
|
treq = wup_resp.newdata.get("tRsp") or wup_resp.newdata.get("tResp")
|
||||||
if not treq:
|
if not treq:
|
||||||
self.logger(f"[RPC] wsLaunch 响应无 tRsp, keys={list(wup_resp.newdata.keys())}")
|
self.logger(
|
||||||
|
f"[RPC] wsLaunch 响应无 tRsp, keys={list(wup_resp.newdata.keys())}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
ins = TafInputStream(treq)
|
ins = TafInputStream(treq)
|
||||||
tag, dtype = ins.peek_head()
|
tag, dtype = ins.peek_head()
|
||||||
if dtype != 0x0a: # STRUCT_BEGIN
|
if dtype != 0x0A: # STRUCT_BEGIN
|
||||||
self.logger(f"[RPC] wsLaunch tRsp 非结构体 dtype=0x{dtype:02x}")
|
self.logger(f"[RPC] wsLaunch tRsp 非结构体 dtype=0x{dtype:02x}")
|
||||||
return
|
return
|
||||||
ins.read_head()
|
ins.read_head()
|
||||||
@@ -376,19 +409,29 @@ class HuyaWssClient:
|
|||||||
ftag, ftype = ins.peek_head()
|
ftag, ftype = ins.peek_head()
|
||||||
except EOFError:
|
except EOFError:
|
||||||
break
|
break
|
||||||
if ftype == 0x0b: # STRUCT_END
|
if ftype == 0x0B: # STRUCT_END
|
||||||
ins.read_head()
|
ins.read_head()
|
||||||
break
|
break
|
||||||
ins.read_head()
|
ins.read_head()
|
||||||
if ftag == 0 and ftype in (0x06, 0x07):
|
if ftag == 0 and ftype in (0x06, 0x07):
|
||||||
ln = ins.buf.read(1)[0] if ftype == 0x06 else int.from_bytes(ins.buf.read(4), 'big')
|
ln = (
|
||||||
self._launch_guid = ins.buf.read(ln).decode('utf-8', 'replace')
|
ins.buf.read(1)[0]
|
||||||
|
if ftype == 0x06
|
||||||
|
else int.from_bytes(ins.buf.read(4), "big")
|
||||||
|
)
|
||||||
|
self._launch_guid = ins.buf.read(ln).decode("utf-8", "replace")
|
||||||
elif ftag == 1 and ftype in (0x06, 0x07):
|
elif ftag == 1 and ftype in (0x06, 0x07):
|
||||||
ln = ins.buf.read(1)[0] if ftype == 0x06 else int.from_bytes(ins.buf.read(4), 'big')
|
ln = (
|
||||||
self._launch_ip = ins.buf.read(ln).decode('utf-8', 'replace')
|
ins.buf.read(1)[0]
|
||||||
|
if ftype == 0x06
|
||||||
|
else int.from_bytes(ins.buf.read(4), "big")
|
||||||
|
)
|
||||||
|
self._launch_ip = ins.buf.read(ln).decode("utf-8", "replace")
|
||||||
else:
|
else:
|
||||||
ins.skip_field(ftype)
|
ins.skip_field(ftype)
|
||||||
self.logger(f"[RPC] wsLaunch 解析: guid={self._launch_guid} ip={self._launch_ip}")
|
self.logger(
|
||||||
|
f"[RPC] wsLaunch 解析: guid={self._launch_guid} ip={self._launch_ip}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger(f"[RPC] wsLaunch 响应解析失败: {e}")
|
self.logger(f"[RPC] wsLaunch 响应解析失败: {e}")
|
||||||
|
|
||||||
@@ -403,27 +446,36 @@ class HuyaWssClient:
|
|||||||
if len(body) < 5:
|
if len(body) < 5:
|
||||||
return body
|
return body
|
||||||
# 大包格式需优先判断;prefix 可能是 0x00,容易被误判成小包长度 8。
|
# 大包格式需优先判断;prefix 可能是 0x00,容易被误判成小包长度 8。
|
||||||
wup_len = int.from_bytes(body[1:5], 'big')
|
wup_len = int.from_bytes(body[1:5], "big")
|
||||||
if 5 + wup_len <= len(body) and body[5:7] == b'\x10\x03':
|
if 5 + wup_len <= len(body) and body[5:7] == b"\x10\x03":
|
||||||
return body[5:5 + wup_len]
|
return body[5 : 5 + wup_len]
|
||||||
# 尝试 4B total_len 小包格式
|
# 尝试 4B total_len 小包格式
|
||||||
total_len = int.from_bytes(body[0:4], 'big')
|
total_len = int.from_bytes(body[0:4], "big")
|
||||||
if 8 <= total_len <= len(body) and body[4:6] == b'\x10\x03':
|
if 8 <= total_len <= len(body) and body[4:6] == b"\x10\x03":
|
||||||
# total_len 包含自身 4B
|
# total_len 包含自身 4B
|
||||||
return body[:total_len]
|
return body[:total_len]
|
||||||
if 5 + wup_len <= len(body):
|
if 5 + wup_len <= len(body):
|
||||||
return body[5:5 + wup_len]
|
return body[5 : 5 + wup_len]
|
||||||
return body
|
return body
|
||||||
|
|
||||||
async def send_auth(self, cookie: str):
|
async def send_auth(self, cookie: str):
|
||||||
"""cmd 0x0a AUTH — 发送 cookie"""
|
"""cmd 0x0a AUTH — 发送 cookie"""
|
||||||
ua = "webh5&0.0.1&websocket&&diypc_52775"
|
ua = "webh5&0.0.1&websocket&&diypc_52775"
|
||||||
auth_text = f"huya_ua={ua}; {cookie}"
|
auth_text = f"huya_ua={ua}; {cookie}"
|
||||||
msg = WssMessage(command=WssCommand.AUTH,
|
msg = WssMessage(
|
||||||
sequence=SEQ_WSLAUNCH,
|
command=WssCommand.AUTH,
|
||||||
body=auth_text.encode('utf-8') + TAIL_BYTES)
|
sequence=SEQ_WSLAUNCH,
|
||||||
|
body=auth_text.encode("utf-8") + TAIL_BYTES,
|
||||||
|
)
|
||||||
await self.ws.send(msg.encode())
|
await self.ws.send(msg.encode())
|
||||||
self.logger(format_wss_log(auth_text.encode('utf-8') + TAIL_BYTES, WssCommand.AUTH, SEQ_WSLAUNCH, "发"))
|
self.logger(
|
||||||
|
format_wss_log(
|
||||||
|
auth_text.encode("utf-8") + TAIL_BYTES,
|
||||||
|
WssCommand.AUTH,
|
||||||
|
SEQ_WSLAUNCH,
|
||||||
|
"发",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async def call_get_config_shop(self, timeout: float = 10.0):
|
async def call_get_config_shop(self, timeout: float = 10.0):
|
||||||
"""商城版 mobileui.getConfig
|
"""商城版 mobileui.getConfig
|
||||||
@@ -441,18 +493,18 @@ class HuyaWssClient:
|
|||||||
os.write_struct_begin(0)
|
os.write_struct_begin(0)
|
||||||
# tag0: UserId (匿名)
|
# tag0: UserId (匿名)
|
||||||
os.write_struct_begin(0)
|
os.write_struct_begin(0)
|
||||||
os.write_int64(0, 0) # lUid = 0
|
os.write_int64(0, 0) # lUid = 0
|
||||||
os.write_string(1, "") # sGuid
|
os.write_string(1, "") # sGuid
|
||||||
os.write_string(2, "") # sUA
|
os.write_string(2, "") # sUA
|
||||||
os.write_string(3, ua) # sHuYaUA
|
os.write_string(3, ua) # sHuYaUA
|
||||||
os.write_string(4, "") # sToken
|
os.write_string(4, "") # sToken
|
||||||
os.write_int32(5, 0) # iTokenType
|
os.write_int32(5, 0) # iTokenType
|
||||||
os.write_string(6, "") # sDeviceInfo
|
os.write_string(6, "") # sDeviceInfo
|
||||||
os.write_string(7, "") # sCookie
|
os.write_string(7, "") # sCookie
|
||||||
os.write_struct_end()
|
os.write_struct_end()
|
||||||
os.write_map(1, {}) # tag1: 空Map
|
os.write_map(1, {}) # tag1: 空Map
|
||||||
os.write_string(2, "huya") # tag2: sAppId
|
os.write_string(2, "huya") # tag2: sAppId
|
||||||
os.write_head(3, TafType.LIST) # tag3: 空List
|
os.write_head(3, TafType.LIST) # tag3: 空List
|
||||||
os.write_int32(0, 0)
|
os.write_int32(0, 0)
|
||||||
os.write_struct_end()
|
os.write_struct_end()
|
||||||
|
|
||||||
@@ -466,8 +518,7 @@ class HuyaWssClient:
|
|||||||
body = self._encode_rpc_body(wup_data)
|
body = self._encode_rpc_body(wup_data)
|
||||||
|
|
||||||
seq = SEQ_GETCONFIG
|
seq = SEQ_GETCONFIG
|
||||||
msg = WssMessage(command=WssCommand.RPC_REQUEST,
|
msg = WssMessage(command=WssCommand.RPC_REQUEST, sequence=seq, body=body)
|
||||||
sequence=seq, body=body)
|
|
||||||
future = asyncio.Future()
|
future = asyncio.Future()
|
||||||
self.rpc_queue.append(future)
|
self.rpc_queue.append(future)
|
||||||
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "发"))
|
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "发"))
|
||||||
@@ -503,8 +554,7 @@ class HuyaWssClient:
|
|||||||
os.write_map(7, {})
|
os.write_map(7, {})
|
||||||
os.write_int32(8, 0)
|
os.write_int32(8, 0)
|
||||||
body = os.get_bytes() + TAIL_BYTES
|
body = os.get_bytes() + TAIL_BYTES
|
||||||
msg = WssMessage(command=WssCommand.REGISTER,
|
msg = WssMessage(command=WssCommand.REGISTER, sequence=SEQ_REGISTER, body=body)
|
||||||
sequence=SEQ_REGISTER, body=body)
|
|
||||||
self.logger(format_wss_log(body, WssCommand.REGISTER, SEQ_REGISTER, "发"))
|
self.logger(format_wss_log(body, WssCommand.REGISTER, SEQ_REGISTER, "发"))
|
||||||
await self.ws.send(msg.encode())
|
await self.ws.send(msg.encode())
|
||||||
|
|
||||||
@@ -513,14 +563,20 @@ class HuyaWssClient:
|
|||||||
os = TafOutputStream()
|
os = TafOutputStream()
|
||||||
os.write_map(0, {})
|
os.write_map(0, {})
|
||||||
body = os.get_bytes() + TAIL_BYTES
|
body = os.get_bytes() + TAIL_BYTES
|
||||||
msg = WssMessage(command=WssCommand.CONFIRM_SEND,
|
msg = WssMessage(
|
||||||
sequence=SEQ_CONFIRM, body=body)
|
command=WssCommand.CONFIRM_SEND, sequence=SEQ_CONFIRM, body=body
|
||||||
|
)
|
||||||
self.logger(format_wss_log(body, WssCommand.CONFIRM_SEND, SEQ_CONFIRM, "发"))
|
self.logger(format_wss_log(body, WssCommand.CONFIRM_SEND, SEQ_CONFIRM, "发"))
|
||||||
await self.ws.send(msg.encode())
|
await self.ws.send(msg.encode())
|
||||||
|
|
||||||
async def call_rpc(self, service: str, method: str,
|
async def call_rpc(
|
||||||
req_struct: TafStruct, rsp_class=None,
|
self,
|
||||||
timeout: float = 10.0):
|
service: str,
|
||||||
|
method: str,
|
||||||
|
req_struct: TafStruct,
|
||||||
|
rsp_class=None,
|
||||||
|
timeout: float = 10.0,
|
||||||
|
):
|
||||||
seq = self._next_biz_seq()
|
seq = self._next_biz_seq()
|
||||||
|
|
||||||
wup = WupRequest()
|
wup = WupRequest()
|
||||||
@@ -532,8 +588,7 @@ class HuyaWssClient:
|
|||||||
wup_data = wup.encode()
|
wup_data = wup.encode()
|
||||||
body = self._encode_rpc_body(wup_data)
|
body = self._encode_rpc_body(wup_data)
|
||||||
|
|
||||||
msg = WssMessage(command=WssCommand.RPC_REQUEST,
|
msg = WssMessage(command=WssCommand.RPC_REQUEST, sequence=seq, body=body)
|
||||||
sequence=seq, body=body)
|
|
||||||
future = asyncio.Future()
|
future = asyncio.Future()
|
||||||
self.rpc_queue.append(future)
|
self.rpc_queue.append(future)
|
||||||
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "发"))
|
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "发"))
|
||||||
@@ -559,7 +614,9 @@ class HuyaWssClient:
|
|||||||
if dtype == TafType.STRUCT_BEGIN:
|
if dtype == TafType.STRUCT_BEGIN:
|
||||||
ins.read_head()
|
ins.read_head()
|
||||||
decoded = _decode_taf_struct(ins)
|
decoded = _decode_taf_struct(ins)
|
||||||
self.logger(f"[←] {service}.{method} {key}: {_truncate(decoded)}")
|
self.logger(
|
||||||
|
f"[←] {service}.{method} {key}: {_truncate(decoded)}"
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -571,10 +628,18 @@ class HuyaWssClient:
|
|||||||
result = wup_resp.readStruct("tResp", rsp_class)
|
result = wup_resp.readStruct("tResp", rsp_class)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def get_goods_info(self, uid: int, guid: str, cookie: str,
|
async def get_goods_info(
|
||||||
pid: int, spu_id: str, sku_id: int = 0,
|
self,
|
||||||
game_id: str = "", source_id: str = "yellowcarlist",
|
uid: int,
|
||||||
scene: int = WSS_SHOP_SCENE):
|
guid: str,
|
||||||
|
cookie: str,
|
||||||
|
pid: int,
|
||||||
|
spu_id: str,
|
||||||
|
sku_id: int = 0,
|
||||||
|
game_id: str = "",
|
||||||
|
source_id: str = "yellowcarlist",
|
||||||
|
scene: int = WSS_SHOP_SCENE,
|
||||||
|
):
|
||||||
from .shop_structs import GetGoodsInfoReqV5, GoodsInfoRsp
|
from .shop_structs import GetGoodsInfoReqV5, GoodsInfoRsp
|
||||||
|
|
||||||
req = GetGoodsInfoReqV5()
|
req = GetGoodsInfoReqV5()
|
||||||
@@ -585,12 +650,20 @@ class HuyaWssClient:
|
|||||||
req.skuId = sku_id
|
req.skuId = sku_id
|
||||||
req.gameId = game_id
|
req.gameId = game_id
|
||||||
|
|
||||||
return await self.call_rpc("shopMiddleUI", "getGoodsInfoV5",
|
return await self.call_rpc(
|
||||||
req, GoodsInfoRsp, timeout=15.0)
|
"shopMiddleUI", "getGoodsInfoV5", req, GoodsInfoRsp, timeout=15.0
|
||||||
|
)
|
||||||
|
|
||||||
async def query_user_order_list(self, uid: int, guid: str, cookie: str,
|
async def query_user_order_list(
|
||||||
offset: int = 0, page_size: int = 10,
|
self,
|
||||||
order_type: int = 1, status: int = 0):
|
uid: int,
|
||||||
|
guid: str,
|
||||||
|
cookie: str,
|
||||||
|
offset: int = 0,
|
||||||
|
page_size: int = 10,
|
||||||
|
order_type: int = 1,
|
||||||
|
status: int = 0,
|
||||||
|
):
|
||||||
from .shop_structs import QueryUserOrderListReq, QueryUserOrderListRsp
|
from .shop_structs import QueryUserOrderListReq, QueryUserOrderListRsp
|
||||||
|
|
||||||
req = QueryUserOrderListReq()
|
req = QueryUserOrderListReq()
|
||||||
@@ -600,17 +673,35 @@ class HuyaWssClient:
|
|||||||
req.pageSize = page_size
|
req.pageSize = page_size
|
||||||
req.status = status
|
req.status = status
|
||||||
|
|
||||||
return await self.call_rpc("revenueWebUI", "queryUserOrderList",
|
return await self.call_rpc(
|
||||||
req, QueryUserOrderListRsp, timeout=15.0)
|
"revenueWebUI",
|
||||||
|
"queryUserOrderList",
|
||||||
|
req,
|
||||||
|
QueryUserOrderListRsp,
|
||||||
|
timeout=15.0,
|
||||||
|
)
|
||||||
|
|
||||||
async def create_order(self, uid: int, guid: str, cookie: str,
|
async def create_order(
|
||||||
pid: int, spu_id: str, sku_id: int,
|
self,
|
||||||
item_count: int = 1, source_id: str = "yellowcarlist",
|
uid: int,
|
||||||
game_id: str = "", scene: int = WSS_SHOP_SCENE,
|
guid: str,
|
||||||
order_type: int = 6):
|
cookie: str,
|
||||||
from .shop_structs import (CreateOrderReqV5, CreateOrderRsp,
|
pid: int,
|
||||||
CreateOrderExtraParam, CreateOrderPromotionParam,
|
spu_id: str,
|
||||||
CreateOrderAccountParam)
|
sku_id: int,
|
||||||
|
item_count: int = 1,
|
||||||
|
source_id: str = "yellowcarlist",
|
||||||
|
game_id: str = "",
|
||||||
|
scene: int = WSS_SHOP_SCENE,
|
||||||
|
order_type: int = 6,
|
||||||
|
):
|
||||||
|
from .shop_structs import (
|
||||||
|
CreateOrderReqV5,
|
||||||
|
CreateOrderRsp,
|
||||||
|
CreateOrderExtraParam,
|
||||||
|
CreateOrderPromotionParam,
|
||||||
|
CreateOrderAccountParam,
|
||||||
|
)
|
||||||
|
|
||||||
req = CreateOrderReqV5()
|
req = CreateOrderReqV5()
|
||||||
req.userId = self._build_biz_user(uid, cookie)
|
req.userId = self._build_biz_user(uid, cookie)
|
||||||
@@ -635,14 +726,22 @@ class HuyaWssClient:
|
|||||||
req.ext = ""
|
req.ext = ""
|
||||||
|
|
||||||
self.logger(f"[下单] orderType={order_type} (HUYA_VIRTUAL)")
|
self.logger(f"[下单] orderType={order_type} (HUYA_VIRTUAL)")
|
||||||
return await self.call_rpc("shopMiddleUI", "createOrderV5",
|
return await self.call_rpc(
|
||||||
req, CreateOrderRsp, timeout=15.0)
|
"shopMiddleUI", "createOrderV5", req, CreateOrderRsp, timeout=15.0
|
||||||
|
)
|
||||||
|
|
||||||
async def pay_order_submit(self, uid: int, guid: str, cookie: str,
|
async def pay_order_submit(
|
||||||
order_id: int, pay_type: int = 1,
|
self,
|
||||||
pid: int = 0, source_id: str = "yellowcarlist",
|
uid: int,
|
||||||
scene: int = WSS_SHOP_SCENE,
|
guid: str,
|
||||||
item_count: int = 1):
|
cookie: str,
|
||||||
|
order_id: int,
|
||||||
|
pay_type: int = 1,
|
||||||
|
pid: int = 0,
|
||||||
|
source_id: str = "yellowcarlist",
|
||||||
|
scene: int = WSS_SHOP_SCENE,
|
||||||
|
item_count: int = 1,
|
||||||
|
):
|
||||||
from .shop_structs import PayOrderRes
|
from .shop_structs import PayOrderRes
|
||||||
|
|
||||||
os = TafOutputStream()
|
os = TafOutputStream()
|
||||||
@@ -674,8 +773,7 @@ class HuyaWssClient:
|
|||||||
wup_data = wup.encode()
|
wup_data = wup.encode()
|
||||||
body = self._encode_rpc_body(wup_data)
|
body = self._encode_rpc_body(wup_data)
|
||||||
|
|
||||||
msg = WssMessage(command=WssCommand.RPC_REQUEST,
|
msg = WssMessage(command=WssCommand.RPC_REQUEST, sequence=seq, body=body)
|
||||||
sequence=seq, body=body)
|
|
||||||
future = asyncio.Future()
|
future = asyncio.Future()
|
||||||
self.rpc_queue.append(future)
|
self.rpc_queue.append(future)
|
||||||
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "发"))
|
self.logger(format_wss_log(body, WssCommand.RPC_REQUEST, seq, "发"))
|
||||||
|
|||||||
Reference in New Issue
Block a user