feat(douyu): support elite handbook lock orders
This commit is contained in:
+126
-32
@@ -3,9 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
@@ -39,7 +37,9 @@ class DouyuActivityClient:
|
||||
BIND_INFO_V2_API = "https://www.douyu.com/japi/carnivalApi/v2/tencent/bindInfo"
|
||||
BIND_API = "https://www.douyu.com/japi/carnivalApi/tencent/bind"
|
||||
GOODS_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/storehome"
|
||||
EXCHANGE_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/exchange"
|
||||
CREATE_EXCHANGE_ORDER_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/createExchangeOrder"
|
||||
PAY_EXCHANGE_ORDER_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/payExchangeOrder"
|
||||
EXCHANGE_ORDER_LIST_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/getExchangeOrderList"
|
||||
EXCHANGE_LIST_API = "https://www.douyu.com/wgapi/ordnc/activity/peace/exchangeList"
|
||||
CREDIT_BALANCE_API = "https://www.douyu.com/japi/oms/web/peace/credit/balance"
|
||||
CREDIT_DIFF_API = "https://www.douyu.com/japi/oms/web/peace/credit/diff"
|
||||
@@ -749,45 +749,139 @@ class DouyuActivityClient:
|
||||
records = (payload.get("data") or {}).get("list") or []
|
||||
return {"records": records, "raw": payload}
|
||||
|
||||
def exchange_goods(self, *, manual_id: str, rid: str, commodity_id: str, ctn: str | None = None) -> dict[str, Any]:
|
||||
"""兑换活动商品。"""
|
||||
ctn_value = ctn or cookie_value(self.cookie, "acf_ccn") or self.acf_ccn(refresh_subscribe=False)
|
||||
@staticmethod
|
||||
def _multipart_fields(data: dict[str, Any]) -> dict[str, tuple[None, str]]:
|
||||
"""Build requests multipart fields without uploading files."""
|
||||
return {key: (None, str(value)) for key, value in data.items()}
|
||||
|
||||
def create_exchange_order(
|
||||
self,
|
||||
*,
|
||||
manual_id: str,
|
||||
rid: str,
|
||||
commodity_id: str,
|
||||
num: int = 1,
|
||||
) -> dict[str, Any]:
|
||||
"""Lock stock for an activity commodity before spending points."""
|
||||
if num < 1:
|
||||
raise DouyuActivityError("锁单数量必须大于 0")
|
||||
token = self.csrf_token()
|
||||
randstr = "".join(random.choices(string.ascii_letters + string.digits, k=16))
|
||||
data = {
|
||||
"manualID": manual_id,
|
||||
"rid": rid,
|
||||
"commodityID": commodity_id,
|
||||
"ctn": ctn_value,
|
||||
"csrfToken": token,
|
||||
"randstr": randstr,
|
||||
"token[error][code]": "-1",
|
||||
"token[data]": "",
|
||||
}
|
||||
payload = self._request_json(
|
||||
"post",
|
||||
self.EXCHANGE_API,
|
||||
"兑换商品",
|
||||
data=data,
|
||||
self.CREATE_EXCHANGE_ORDER_API,
|
||||
"锁定兑换商品",
|
||||
files=self._multipart_fields({
|
||||
"manualID": manual_id,
|
||||
"rid": rid,
|
||||
"commodityID": commodity_id,
|
||||
"num": num,
|
||||
"csrfToken": token,
|
||||
}),
|
||||
headers={
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"Referer": f"https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0&roomId={rid}",
|
||||
"sec-ch-ua": f'"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"Windows"',
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
},
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
raise DouyuActivityError(payload.get("msg") or "兑换商品失败")
|
||||
raise DouyuActivityError(payload.get("msg") or "锁定兑换商品失败")
|
||||
data = payload.get("data") or {}
|
||||
order_id = str(data.get("orderId") or "").strip()
|
||||
if not order_id:
|
||||
raise DouyuActivityError("锁定兑换商品成功,但响应中没有 orderId")
|
||||
locked_at = int(time.time())
|
||||
try:
|
||||
expire_seconds = int(data.get("expireSeconds") or 0)
|
||||
except (TypeError, ValueError):
|
||||
expire_seconds = 0
|
||||
if expire_seconds > 0:
|
||||
expires_at = locked_at + expire_seconds
|
||||
else:
|
||||
expires_at = None
|
||||
return {
|
||||
"order_id": order_id,
|
||||
"commodity_id": commodity_id,
|
||||
"commodity_name": data.get("commodityName") or "",
|
||||
"commodity_image": data.get("commodityImage") or "",
|
||||
"score": data.get("score"),
|
||||
"num": data.get("num", num),
|
||||
"expire_seconds": data.get("expireSeconds"),
|
||||
"locked_at": locked_at,
|
||||
"expires_at": expires_at,
|
||||
"csrf_token": token,
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
def pay_exchange_order(self, *, manual_id: str, order_id: str) -> dict[str, Any]:
|
||||
"""Pay a previously locked exchange order with handbook points."""
|
||||
normalized_order_id = str(order_id).strip()
|
||||
if not normalized_order_id:
|
||||
raise DouyuActivityError("锁单订单号不能为空")
|
||||
token = self.csrf_token()
|
||||
payload = self._request_json(
|
||||
"post",
|
||||
self.PAY_EXCHANGE_ORDER_API,
|
||||
"支付锁单",
|
||||
files=self._multipart_fields({
|
||||
"orderID": normalized_order_id,
|
||||
"manualID": manual_id,
|
||||
"csrfToken": token,
|
||||
}),
|
||||
headers={
|
||||
"Referer": "https://www.douyu.com/pages/live-peace-handbook/web/shop?ditchname=pass0",
|
||||
},
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
raise DouyuActivityError(payload.get("msg") or "支付锁单失败")
|
||||
data = payload.get("data") or {}
|
||||
return {
|
||||
"order_id": str(data.get("orderId") or normalized_order_id),
|
||||
"exchange_id": str(data.get("exchangeId") or ""),
|
||||
"commodity_type": data.get("commodityType"),
|
||||
"commodity_image": data.get("commodityImage") or "",
|
||||
"exchange_num": data.get("exchangeNum"),
|
||||
"s_type": data.get("sType"),
|
||||
"g_type": data.get("gType"),
|
||||
"csrf_token": token,
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
def exchange_orders(self, *, manual_id: str, page: int = 1, page_size: int = 10) -> dict[str, Any]:
|
||||
"""Query current locked exchange orders."""
|
||||
payload = self._request_json(
|
||||
"get",
|
||||
self.EXCHANGE_ORDER_LIST_API,
|
||||
"查询锁单",
|
||||
params={"page": page, "pageSize": page_size, "manualID": manual_id},
|
||||
headers={
|
||||
"Origin": "",
|
||||
"Referer": "https://www.douyu.com/pages/live-peace-handbook/web/lock-order?ditchname=pass0",
|
||||
},
|
||||
)
|
||||
if payload.get("error") not in (0, "0"):
|
||||
raise DouyuActivityError(payload.get("msg") or "查询锁单失败")
|
||||
data = payload.get("data") or {}
|
||||
return {
|
||||
"orders": data.get("list") or [],
|
||||
"total": data.get("total") or 0,
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
def exchange_goods(self, *, manual_id: str, rid: str, commodity_id: str) -> dict[str, Any]:
|
||||
"""Lock an activity commodity and immediately pay the locked order."""
|
||||
locked = self.create_exchange_order(
|
||||
manual_id=manual_id,
|
||||
rid=rid,
|
||||
commodity_id=commodity_id,
|
||||
num=1,
|
||||
)
|
||||
paid = self.pay_exchange_order(manual_id=manual_id, order_id=locked["order_id"])
|
||||
return {
|
||||
"commodity_id": commodity_id,
|
||||
"ctn": ctn_value,
|
||||
"csrf_token": token,
|
||||
"randstr": randstr,
|
||||
"raw": payload,
|
||||
"order_id": locked["order_id"],
|
||||
"exchange_id": paid["exchange_id"],
|
||||
"commodity_image": paid["commodity_image"] or locked["commodity_image"],
|
||||
"exchange_num": paid["exchange_num"],
|
||||
"lock_order": locked,
|
||||
"payment": paid,
|
||||
}
|
||||
|
||||
def gold_account(self) -> dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user