|
@@ -0,0 +1,842 @@
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
|
|
+"""
|
|
|
|
|
+复现 client-tauri 扫货购买 + 手机令牌确认流程(对照 buff_sweep_runner.rs::purchase_one_listing)。
|
|
|
|
|
+
|
|
|
|
|
+流程(与客户端一致):
|
|
|
|
|
+ 1) POST http://localhost:5000/login → 拿 session / steamLoginSecure
|
|
|
|
|
+ 2) (可选)GET market/.../render → 用 listing_id 取价格
|
|
|
|
|
+ 3) POST steamcommunity.com/market/buylisting/{listing_id}
|
|
|
|
|
+ confirmation=0
|
|
|
|
|
+ → 若 wallet_info.success==1:无需确认,成功结束
|
|
|
|
|
+ → 若 need_confirmation==true:取出 confirmation.confirmation_id
|
|
|
|
|
+ 4) POST http://localhost:5000/trade-confirmations/operate
|
|
|
|
|
+ Operation=accept, ConfirmationId=confirmation_id
|
|
|
|
|
+ (steamAPI 内部会 FetchConfirmations,按 id 或 creator_id 匹配后 AcceptConfirmation)
|
|
|
|
|
+ 5) 再次 POST buylisting/{listing_id}
|
|
|
|
|
+ confirmation=<confirmation_id>
|
|
|
|
|
+ → 检查 wallet_info.success==1
|
|
|
|
|
+
|
|
|
|
|
+依赖:
|
|
|
|
|
+ - 本地 steamAPI 已启动: dotnet run --urls "http://localhost:5000"
|
|
|
|
|
+ - 该账号的 .maFile(含 identity_secret / device_id)
|
|
|
|
|
+ - requests
|
|
|
|
|
+
|
|
|
|
|
+用法示例:
|
|
|
|
|
+ # 使用本目录 config.json(推荐,服务器部署):
|
|
|
|
|
+ python steam_buy_confirm_test.py --listing-id 123 --market-url "https://..." --proxy 1.2.3.4:8080
|
|
|
|
|
+
|
|
|
|
|
+ python steam_buy_confirm_test.py ^
|
|
|
|
|
+ --username YOUR_STEAM_USER ^
|
|
|
|
|
+ --password YOUR_STEAM_PASS ^
|
|
|
|
|
+ --mafile "D:\\path\\to\\account.maFile" ^
|
|
|
|
|
+ --listing-id 1234567890123456789 ^
|
|
|
|
|
+ --market-url "https://steamcommunity.com/market/listings/730/ItemName"
|
|
|
|
|
+
|
|
|
|
|
+ # 已知价格(美分)时可跳过 render:
|
|
|
|
|
+ python steam_buy_confirm_test.py ... --listing-id ... --subtotal 100 --steam-fee 10 --pub-fee 5
|
|
|
|
|
+
|
|
|
|
|
+ # 默认非美区(仅空 billing_state,与浏览器一致);美区账号才加:
|
|
|
|
|
+ # --us-account
|
|
|
|
|
+
|
|
|
|
|
+ # 只测确认(已有 confirmation_id,不真正下单):
|
|
|
|
|
+ python steam_buy_confirm_test.py ... --confirm-only --confirmation-id 1234567890
|
|
|
|
|
+
|
|
|
|
|
+ # 代理:仅市场取价/购买走代理;登录与令牌确认不走代理
|
|
|
|
|
+ python steam_buy_confirm_test.py ... --proxy 154.48.255.25:13465
|
|
|
|
|
+ python steam_buy_confirm_test.py ... --proxy http://154.48.255.25:13465
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+from __future__ import annotations
|
|
|
|
|
+
|
|
|
|
|
+import argparse
|
|
|
|
|
+import json
|
|
|
|
|
+import random
|
|
|
|
|
+import sys
|
|
|
|
|
+import time
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+from typing import Any, Dict, Optional, Tuple
|
|
|
|
|
+try:
|
|
|
|
|
+ import requests
|
|
|
|
|
+except ImportError:
|
|
|
|
|
+ print("请先安装: pip install requests", file=sys.stderr)
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+STEAM_API_BASE_DEFAULT = "http://localhost:5000"
|
|
|
|
|
+BUY_URL = "https://steamcommunity.com/market/buylisting/{listing_id}"
|
|
|
|
|
+# 对齐常见 Chrome 指纹(与浏览器抓包一致)
|
|
|
|
|
+UA = (
|
|
|
|
|
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
|
|
|
+ "(KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
|
|
|
|
|
+)
|
|
|
|
|
+SEC_CH_UA = '"Not=A?Brand";v="99", "Google Chrome";v="151", "Chromium";v="151"'
|
|
|
|
|
+ROOT_DIR = Path(__file__).resolve().parent
|
|
|
|
|
+DEFAULT_CONFIG = ROOT_DIR / "config.json"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def log(step: str, msg: str, data: Any = None) -> None:
|
|
|
|
|
+ print(f"\n===== [{step}] {msg} =====")
|
|
|
|
|
+ if data is not None:
|
|
|
|
|
+ if isinstance(data, (dict, list)):
|
|
|
|
|
+ print(json.dumps(data, ensure_ascii=False, indent=2)[:4000])
|
|
|
|
|
+ else:
|
|
|
|
|
+ text = str(data)
|
|
|
|
|
+ print(text if len(text) <= 4000 else text[:4000] + "...(truncated)")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def clip(text: str, n: int = 500) -> str:
|
|
|
|
|
+ text = text or ""
|
|
|
|
|
+ return text if len(text) <= n else text[:n] + "..."
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def random_us_addr() -> Dict[str, str]:
|
|
|
|
|
+ """对应 buff_sweep_runner.rs::random_addr_fields(美区账号账单地址)。"""
|
|
|
|
|
+ return {
|
|
|
|
|
+ "first_name": f"user{random.randint(1000, 9999)}",
|
|
|
|
|
+ "last_name": f"ln{random.randint(1000, 9999)}",
|
|
|
|
|
+ "billing_address": f"{random.randint(10, 9999)} Main St",
|
|
|
|
|
+ "billing_address_two": f"Apt {random.randint(1, 999)}",
|
|
|
|
|
+ "billing_country": "US",
|
|
|
|
|
+ "billing_city": f"City{random.randint(10, 999)}",
|
|
|
|
|
+ "billing_state": "AK",
|
|
|
|
|
+ "billing_postal_code": str(random.randint(10000, 99999)),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class SessionState:
|
|
|
|
|
+ def __init__(self) -> None:
|
|
|
|
|
+ self.steam_id: int = 0
|
|
|
|
|
+ self.access_token: str = ""
|
|
|
|
|
+ self.refresh_token: str = ""
|
|
|
|
|
+ self.session_id: str = ""
|
|
|
|
|
+ self.steam_login_secure: str = ""
|
|
|
|
|
+ self.mafile_path: str = ""
|
|
|
|
|
+ # 浏览器侧常见附属 Cookie(非登录必需,降低“脚本味”)
|
|
|
|
|
+ self.browser_id: str = str(random.randint(10**17, 10**18 - 1))
|
|
|
|
|
+ self.steam_country: str = "HK%7C795cd6413f372e6c27f32346040e5249"
|
|
|
|
|
+
|
|
|
|
|
+ def apply_login_response(self, resp: dict) -> None:
|
|
|
|
|
+ sess = resp.get("session") or {}
|
|
|
|
|
+ self.steam_id = int(sess.get("steamId") or 0)
|
|
|
|
|
+ self.access_token = sess.get("accessToken") or ""
|
|
|
|
|
+ self.refresh_token = sess.get("refreshToken") or ""
|
|
|
|
|
+ self.session_id = sess.get("sessionId") or self.session_id
|
|
|
|
|
+
|
|
|
|
|
+ for c in resp.get("cookies") or []:
|
|
|
|
|
+ name = c.get("name")
|
|
|
|
|
+ value = c.get("value") or ""
|
|
|
|
|
+ if name == "steamLoginSecure" and value:
|
|
|
|
|
+ self.steam_login_secure = value
|
|
|
|
|
+ if name == "sessionid" and value:
|
|
|
|
|
+ self.session_id = value
|
|
|
|
|
+ if name == "browserid" and value:
|
|
|
|
|
+ self.browser_id = value
|
|
|
|
|
+ if name == "steamCountry" and value:
|
|
|
|
|
+ self.steam_country = value
|
|
|
|
|
+
|
|
|
|
|
+ # 与 SessionData.GetSteamLoginSecure 一致:steamid%7C%7CaccessToken
|
|
|
|
|
+ if not self.steam_login_secure and self.steam_id and self.access_token:
|
|
|
|
|
+ self.steam_login_secure = f"{self.steam_id}%7C%7C{self.access_token}"
|
|
|
|
|
+
|
|
|
|
|
+ def apply_operate_session(self, resp: dict) -> None:
|
|
|
|
|
+ sess = resp.get("session") or {}
|
|
|
|
|
+ if sess.get("sessionId"):
|
|
|
|
|
+ self.session_id = sess["sessionId"]
|
|
|
|
|
+ if sess.get("accessToken"):
|
|
|
|
|
+ self.access_token = sess["accessToken"]
|
|
|
|
|
+ if sess.get("refreshToken"):
|
|
|
|
|
+ self.refresh_token = sess["refreshToken"]
|
|
|
|
|
+ if sess.get("steamId"):
|
|
|
|
|
+ self.steam_id = int(sess["steamId"])
|
|
|
|
|
+ if self.steam_id and self.access_token:
|
|
|
|
|
+ self.steam_login_secure = f"{self.steam_id}%7C%7C{self.access_token}"
|
|
|
|
|
+
|
|
|
|
|
+ def cookie_header(self, for_market: bool = True) -> str:
|
|
|
|
|
+ """尽量贴近浏览器市场请求 Cookie 组合。"""
|
|
|
|
|
+ parts = [
|
|
|
|
|
+ "ActListPageSize=10",
|
|
|
|
|
+ "timezoneOffset=28800,0",
|
|
|
|
|
+ "timezoneName=Asia%2FShanghai",
|
|
|
|
|
+ f"browserid={self.browser_id}",
|
|
|
|
|
+ f"steamCountry={self.steam_country}",
|
|
|
|
|
+ f"sessionid={self.session_id}",
|
|
|
|
|
+ f"steamLoginSecure={self.steam_login_secure}",
|
|
|
|
|
+ ]
|
|
|
|
|
+ if for_market:
|
|
|
|
|
+ parts.insert(1, "bMarketOptOut=1")
|
|
|
|
|
+ parts.append("strInventoryLastContext=730_2")
|
|
|
|
|
+ return "; ".join(parts) + ";"
|
|
|
|
|
+
|
|
|
|
|
+ def to_steam_api_session(self) -> dict:
|
|
|
|
|
+ return {
|
|
|
|
|
+ "SteamId": self.steam_id,
|
|
|
|
|
+ "AccessToken": self.access_token,
|
|
|
|
|
+ "RefreshToken": self.refresh_token,
|
|
|
|
|
+ "SessionId": self.session_id,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def browser_market_headers(referer: str, cookie: str) -> Dict[str, str]:
|
|
|
|
|
+ """对齐浏览器对 steamcommunity.com/market 的 XHR 指纹头。"""
|
|
|
|
|
+ return {
|
|
|
|
|
+ "Accept": "*/*",
|
|
|
|
|
+ "Accept-Language": "zh-CN,zh;q=0.9",
|
|
|
|
|
+ "Accept-Encoding": "gzip, deflate",
|
|
|
|
|
+ "Connection": "keep-alive",
|
|
|
|
|
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
|
|
|
+ "Origin": "https://steamcommunity.com",
|
|
|
|
|
+ "Referer": referer,
|
|
|
|
|
+ "Cookie": cookie,
|
|
|
|
|
+ "User-Agent": UA,
|
|
|
|
|
+ "Sec-Fetch-Dest": "empty",
|
|
|
|
|
+ "Sec-Fetch-Mode": "cors",
|
|
|
|
|
+ "Sec-Fetch-Site": "same-origin",
|
|
|
|
|
+ "sec-ch-ua": SEC_CH_UA,
|
|
|
|
|
+ "sec-ch-ua-mobile": "?0",
|
|
|
|
|
+ "sec-ch-ua-platform": '"Windows"',
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def steam_api_login(
|
|
|
|
|
+ api_base: str,
|
|
|
|
|
+ username: str,
|
|
|
|
|
+ password: str,
|
|
|
|
|
+ mafile: str,
|
|
|
|
|
+ proxy_url: Optional[str] = None,
|
|
|
|
|
+) -> SessionState:
|
|
|
|
|
+ url = f"{api_base.rstrip('/')}/login"
|
|
|
|
|
+ body: Dict[str, Any] = {
|
|
|
|
|
+ "Username": username,
|
|
|
|
|
+ "Password": password,
|
|
|
|
|
+ "MaFilePath": mafile,
|
|
|
|
|
+ }
|
|
|
|
|
+ if proxy_url:
|
|
|
|
|
+ body["ProxyUrl"] = proxy_url
|
|
|
|
|
+ log("1-login", f"POST {url}", {"Username": username, "MaFilePath": mafile})
|
|
|
|
|
+ r = requests.post(url, json=body, timeout=60)
|
|
|
|
|
+ text = r.text
|
|
|
|
|
+ log("1-login", f"HTTP {r.status_code}", clip(text, 1500))
|
|
|
|
|
+ if r.status_code != 200:
|
|
|
|
|
+ raise RuntimeError(f"login 失败: HTTP {r.status_code} {clip(text)}")
|
|
|
|
|
+ data = r.json()
|
|
|
|
|
+ st = SessionState()
|
|
|
|
|
+ st.mafile_path = mafile
|
|
|
|
|
+ st.apply_login_response(data)
|
|
|
|
|
+ if not st.session_id or not st.steam_login_secure or not st.access_token:
|
|
|
|
|
+ raise RuntimeError(f"login 响应缺字段: {json.dumps(data, ensure_ascii=False)[:800]}")
|
|
|
|
|
+ log(
|
|
|
|
|
+ "1-login",
|
|
|
|
|
+ "session OK",
|
|
|
|
|
+ {
|
|
|
|
|
+ "steamId": st.steam_id,
|
|
|
|
|
+ "sessionId": st.session_id,
|
|
|
|
|
+ "steamLoginSecure_len": len(st.steam_login_secure),
|
|
|
|
|
+ "accessToken_len": len(st.access_token),
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
|
|
+ return st
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def normalize_proxy(proxy: Optional[str]) -> Optional[str]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 规范化代理地址,供 steamAPI ProxyUrl 与 requests 使用。
|
|
|
|
|
+ 支持:
|
|
|
|
|
+ - 154.48.255.25:13465
|
|
|
|
|
+ - http://154.48.255.25:13465
|
|
|
|
|
+ - https://host:port / socks5://host:port
|
|
|
|
|
+ """
|
|
|
|
|
+ if proxy is None:
|
|
|
|
|
+ return None
|
|
|
|
|
+ raw = str(proxy).strip()
|
|
|
|
|
+ if not raw:
|
|
|
|
|
+ return None
|
|
|
|
|
+ lower = raw.lower()
|
|
|
|
|
+ if lower.startswith(("http://", "https://", "socks5://", "socks4://", "socks://")):
|
|
|
|
|
+ return raw
|
|
|
|
|
+ # host:port(可含 IPv6 方括号,如 [2001:db8::1]:8080)
|
|
|
|
|
+ if "://" not in raw:
|
|
|
|
|
+ return f"http://{raw}"
|
|
|
|
|
+ return raw
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def http_proxies(proxy_url: Optional[str]) -> Optional[Dict[str, str]]:
|
|
|
|
|
+ """HTTP(S) 代理,如 http://154.48.255.25:13465。"""
|
|
|
|
|
+ proxy_url = normalize_proxy(proxy_url)
|
|
|
|
|
+ if not proxy_url:
|
|
|
|
|
+ return None
|
|
|
|
|
+ return {"http": proxy_url, "https": proxy_url}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def fetch_listing_price(
|
|
|
|
|
+ market_url: str,
|
|
|
|
|
+ listing_id: str,
|
|
|
|
|
+ session: SessionState,
|
|
|
|
|
+ proxy_url: Optional[str] = None,
|
|
|
|
|
+) -> Tuple[int, int, int]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 从 market listing render 取 converted_price / steam_fee / publisher_fee(美分)。
|
|
|
|
|
+ 对应客户端扫货取价逻辑。
|
|
|
|
|
+ """
|
|
|
|
|
+ base = market_url.rstrip("/")
|
|
|
|
|
+ if not base.endswith("/render"):
|
|
|
|
|
+ base = base + "/render"
|
|
|
|
|
+ url = f"{base}/?query=&start=0&count=100&country=US&language=schinese¤cy=1"
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ "User-Agent": UA,
|
|
|
|
|
+ "Cookie": session.cookie_header(for_market=True),
|
|
|
|
|
+ "Referer": market_url.split("/render")[0],
|
|
|
|
|
+ "Accept": "text/javascript, text/html, application/xml, text/xml, */*",
|
|
|
|
|
+ "Accept-Language": "zh-CN,zh;q=0.9",
|
|
|
|
|
+ "X-Requested-With": "XMLHttpRequest",
|
|
|
|
|
+ "Sec-Fetch-Dest": "empty",
|
|
|
|
|
+ "Sec-Fetch-Mode": "cors",
|
|
|
|
|
+ "Sec-Fetch-Site": "same-origin",
|
|
|
|
|
+ "sec-ch-ua": SEC_CH_UA,
|
|
|
|
|
+ "sec-ch-ua-mobile": "?0",
|
|
|
|
|
+ "sec-ch-ua-platform": '"Windows"',
|
|
|
|
|
+ }
|
|
|
|
|
+ log("2-render", f"GET {url}", {"proxy": proxy_url or "direct"})
|
|
|
|
|
+ r = requests.get(url, headers=headers, timeout=30, proxies=http_proxies(proxy_url))
|
|
|
|
|
+ log("2-render", f"HTTP {r.status_code}", clip(r.text, 800))
|
|
|
|
|
+ r.raise_for_status()
|
|
|
|
|
+ page = r.json()
|
|
|
|
|
+ listing = (page.get("listinginfo") or {}).get(str(listing_id))
|
|
|
|
|
+ if not listing:
|
|
|
|
|
+ # 有时 key 是数字型,再扫一遍
|
|
|
|
|
+ for k, v in (page.get("listinginfo") or {}).items():
|
|
|
|
|
+ if str(k) == str(listing_id):
|
|
|
|
|
+ listing = v
|
|
|
|
|
+ break
|
|
|
|
|
+ if not listing:
|
|
|
|
|
+ raise RuntimeError(
|
|
|
|
|
+ f"render 中找不到 listing_id={listing_id}。"
|
|
|
|
|
+ f"当前 keys={list((page.get('listinginfo') or {}).keys())[:20]}"
|
|
|
|
|
+ )
|
|
|
|
|
+ subtotal = int(listing.get("converted_price") or 0)
|
|
|
|
|
+ steam_fee = int(listing.get("converted_steam_fee") or 0)
|
|
|
|
|
+ pub_fee = int(listing.get("converted_publisher_fee") or 0)
|
|
|
|
|
+ if subtotal <= 0:
|
|
|
|
|
+ raise RuntimeError(f"listing 价格无效: {listing}")
|
|
|
|
|
+ log(
|
|
|
|
|
+ "2-render",
|
|
|
|
|
+ "价格",
|
|
|
|
|
+ {
|
|
|
|
|
+ "listing_id": listing_id,
|
|
|
|
|
+ "subtotal": subtotal,
|
|
|
|
|
+ "steam_fee": steam_fee,
|
|
|
|
|
+ "pub_fee": pub_fee,
|
|
|
|
|
+ "total": subtotal + steam_fee + pub_fee,
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
|
|
+ return subtotal, steam_fee, pub_fee
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def post_buylisting(
|
|
|
|
|
+ session: SessionState,
|
|
|
|
|
+ listing_id: str,
|
|
|
|
|
+ referer: str,
|
|
|
|
|
+ subtotal: int,
|
|
|
|
|
+ fee: int,
|
|
|
|
|
+ total: int,
|
|
|
|
|
+ confirmation: str,
|
|
|
|
|
+ is_us_account: bool,
|
|
|
|
|
+ label: str,
|
|
|
|
|
+ proxy_url: Optional[str] = None,
|
|
|
|
|
+) -> Tuple[int, str, dict]:
|
|
|
|
|
+ """对应 buff_sweep_runner.rs::post_buylisting。"""
|
|
|
|
|
+ url = BUY_URL.format(listing_id=listing_id)
|
|
|
|
|
+ form: Dict[str, str] = {
|
|
|
|
|
+ "sessionid": session.session_id,
|
|
|
|
|
+ "currency": "1",
|
|
|
|
|
+ "subtotal": str(subtotal),
|
|
|
|
|
+ "fee": str(fee),
|
|
|
|
|
+ "total": str(total),
|
|
|
|
|
+ "quantity": "1",
|
|
|
|
|
+ "save_my_address": "0",
|
|
|
|
|
+ "tradefee_tax": "0",
|
|
|
|
|
+ "confirmation": str(confirmation),
|
|
|
|
|
+ }
|
|
|
|
|
+ if is_us_account:
|
|
|
|
|
+ form.update(random_us_addr())
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 与浏览器一致:只传空 billing_state,不附带美区地址字段
|
|
|
|
|
+ form["billing_state"] = ""
|
|
|
|
|
+
|
|
|
|
|
+ headers = browser_market_headers(referer, session.cookie_header(for_market=True))
|
|
|
|
|
+ log(
|
|
|
|
|
+ label,
|
|
|
|
|
+ f"POST {url}",
|
|
|
|
|
+ {
|
|
|
|
|
+ "confirmation": confirmation,
|
|
|
|
|
+ "subtotal": subtotal,
|
|
|
|
|
+ "fee": fee,
|
|
|
|
|
+ "total": total,
|
|
|
|
|
+ "is_us_account": is_us_account,
|
|
|
|
|
+ "billing_fields": (
|
|
|
|
|
+ "random_us_addr"
|
|
|
|
|
+ if is_us_account
|
|
|
|
|
+ else "billing_state="
|
|
|
|
|
+ ),
|
|
|
|
|
+ "proxy": proxy_url or "direct",
|
|
|
|
|
+ "ua": UA,
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
|
|
+ r = requests.post(
|
|
|
|
|
+ url, data=form, headers=headers, timeout=30, proxies=http_proxies(proxy_url)
|
|
|
|
|
+ )
|
|
|
|
|
+ text = r.text
|
|
|
|
|
+ log(label, f"HTTP {r.status_code}", clip(text, 2000))
|
|
|
|
|
+ parsed: dict = {}
|
|
|
|
|
+ try:
|
|
|
|
|
+ if text.strip():
|
|
|
|
|
+ data = r.json()
|
|
|
|
|
+ parsed = data if isinstance(data, dict) else {"_raw": data}
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ parsed = {"_raw": text}
|
|
|
|
|
+ return r.status_code, text, parsed
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def wallet_success(parsed: Optional[dict]) -> bool:
|
|
|
|
|
+ if not isinstance(parsed, dict):
|
|
|
|
|
+ return False
|
|
|
|
|
+ w = parsed.get("wallet_info")
|
|
|
|
|
+ if isinstance(w, dict):
|
|
|
|
|
+ return w.get("success") in (1, "1", True)
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def extract_confirmation_id(parsed: Optional[dict]) -> str:
|
|
|
|
|
+ if not isinstance(parsed, dict):
|
|
|
|
|
+ return ""
|
|
|
|
|
+ conf = parsed.get("confirmation") or {}
|
|
|
|
|
+ if not isinstance(conf, dict):
|
|
|
|
|
+ return ""
|
|
|
|
|
+ cid = conf.get("confirmation_id")
|
|
|
|
|
+ if cid is None:
|
|
|
|
|
+ return ""
|
|
|
|
|
+ return str(cid).strip()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def is_rate_limited(status: int, raw: str, parsed: Optional[dict]) -> bool:
|
|
|
|
|
+ if status == 429:
|
|
|
|
|
+ return True
|
|
|
|
|
+ if isinstance(parsed, dict) and str(parsed.get("message", "")).lower().find("too many") >= 0:
|
|
|
|
|
+ return True
|
|
|
|
|
+ return "too many requests" in (raw or "").lower()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def operate_confirmation(
|
|
|
|
|
+ api_base: str,
|
|
|
|
|
+ session: SessionState,
|
|
|
|
|
+ confirmation_id: str,
|
|
|
|
|
+ max_retries: int = 10,
|
|
|
|
|
+ proxy_url: Optional[str] = None,
|
|
|
|
|
+) -> bool:
|
|
|
|
|
+ """对应 operate_confirmation_with_retry → steamAPI /trade-confirmations/operate。"""
|
|
|
|
|
+ url = f"{api_base.rstrip('/')}/trade-confirmations/operate"
|
|
|
|
|
+ # ConfirmationId 在 C# 侧是 ulong;buylisting 有时返回字符串数字
|
|
|
|
|
+ try:
|
|
|
|
|
+ cid_num = int(confirmation_id)
|
|
|
|
|
+ except ValueError as e:
|
|
|
|
|
+ raise RuntimeError(f"confirmation_id 无效: {confirmation_id}") from e
|
|
|
|
|
+
|
|
|
|
|
+ for attempt in range(1, max_retries + 1):
|
|
|
|
|
+ body: Dict[str, Any] = {
|
|
|
|
|
+ "MaFilePath": session.mafile_path,
|
|
|
|
|
+ "Session": session.to_steam_api_session(),
|
|
|
|
|
+ "ConfirmationId": cid_num,
|
|
|
|
|
+ "Operation": "accept",
|
|
|
|
|
+ }
|
|
|
|
|
+ if proxy_url:
|
|
|
|
|
+ body["ProxyUrl"] = proxy_url
|
|
|
|
|
+ log(
|
|
|
|
|
+ "4-confirm",
|
|
|
|
|
+ f"attempt {attempt}/{max_retries} POST {url}",
|
|
|
|
|
+ {"ConfirmationId": cid_num, "Operation": "accept"},
|
|
|
|
|
+ )
|
|
|
|
|
+ try:
|
|
|
|
|
+ r = requests.post(url, json=body, timeout=30)
|
|
|
|
|
+ text = r.text
|
|
|
|
|
+ log("4-confirm", f"HTTP {r.status_code}", clip(text, 1500))
|
|
|
|
|
+ if r.status_code != 200:
|
|
|
|
|
+ if attempt < max_retries:
|
|
|
|
|
+ time.sleep(3)
|
|
|
|
|
+ continue
|
|
|
|
|
+ return False
|
|
|
|
|
+ data = r.json() if text.strip().startswith("{") else {}
|
|
|
|
|
+ session.apply_operate_session(data)
|
|
|
|
|
+ ok = data.get("success") in (True, 1, "1")
|
|
|
|
|
+ if ok:
|
|
|
|
|
+ log(
|
|
|
|
|
+ "4-confirm",
|
|
|
|
|
+ "确认成功",
|
|
|
|
|
+ {
|
|
|
|
|
+ "matchedBy": data.get("matchedBy"),
|
|
|
|
|
+ "confirmationId": data.get("confirmationId"),
|
|
|
|
|
+ "requestedConfirmationId": data.get("requestedConfirmationId"),
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
|
|
+ return True
|
|
|
|
|
+ if attempt < max_retries:
|
|
|
|
|
+ time.sleep(3)
|
|
|
|
|
+ continue
|
|
|
|
|
+ return False
|
|
|
|
|
+ except requests.RequestException as e:
|
|
|
|
|
+ log("4-confirm", f"请求异常: {e}")
|
|
|
|
|
+ if attempt < max_retries:
|
|
|
|
|
+ time.sleep(3)
|
|
|
|
|
+ continue
|
|
|
|
|
+ raise
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def list_confirmations(api_base: str, session: SessionState, proxy_url: Optional[str] = None) -> list:
|
|
|
|
|
+ url = f"{api_base.rstrip('/')}/trade-confirmations/list"
|
|
|
|
|
+ body: Dict[str, Any] = {
|
|
|
|
|
+ "MaFilePath": session.mafile_path,
|
|
|
|
|
+ "Session": session.to_steam_api_session(),
|
|
|
|
|
+ }
|
|
|
|
|
+ if proxy_url:
|
|
|
|
|
+ body["ProxyUrl"] = proxy_url
|
|
|
|
|
+ log("confirm-list", f"POST {url}")
|
|
|
|
|
+ r = requests.post(url, json=body, timeout=30)
|
|
|
|
|
+ log("confirm-list", f"HTTP {r.status_code}", clip(r.text, 2000))
|
|
|
|
|
+ if r.status_code != 200:
|
|
|
|
|
+ return []
|
|
|
|
|
+ data = r.json()
|
|
|
|
|
+ session.apply_operate_session(data)
|
|
|
|
|
+ return data.get("confirmations") or []
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def purchase_one_listing(
|
|
|
|
|
+ api_base: str,
|
|
|
|
|
+ session: SessionState,
|
|
|
|
|
+ listing_id: str,
|
|
|
|
|
+ referer: str,
|
|
|
|
|
+ subtotal: int,
|
|
|
|
|
+ steam_fee: int,
|
|
|
|
|
+ pub_fee: int,
|
|
|
|
|
+ is_us_account: bool,
|
|
|
|
|
+ confirm_retries: int = 10,
|
|
|
|
|
+ buy2_retries: int = 10,
|
|
|
|
|
+ proxy_url: Optional[str] = None,
|
|
|
|
|
+ buy_retries: int = 8,
|
|
|
|
|
+) -> Tuple[bool, str]:
|
|
|
|
|
+ """
|
|
|
|
|
+ 严格复现 purchase_one_listing:
|
|
|
|
|
+ buy(confirmation=0) → 可选 confirm → buy2(confirmation=id)
|
|
|
|
|
+ """
|
|
|
|
|
+ fee = steam_fee + pub_fee
|
|
|
|
|
+ total = subtotal + fee
|
|
|
|
|
+
|
|
|
|
|
+ status, raw, first = 0, "", {}
|
|
|
|
|
+ for attempt in range(1, buy_retries + 1):
|
|
|
|
|
+ status, raw, first = post_buylisting(
|
|
|
|
|
+ session,
|
|
|
|
|
+ listing_id,
|
|
|
|
|
+ referer,
|
|
|
|
|
+ subtotal,
|
|
|
|
|
+ fee,
|
|
|
|
|
+ total,
|
|
|
|
|
+ "0",
|
|
|
|
|
+ is_us_account,
|
|
|
|
|
+ f"3-buy#{attempt}" if buy_retries > 1 else "3-buy",
|
|
|
|
|
+ proxy_url=proxy_url,
|
|
|
|
|
+ )
|
|
|
|
|
+ if wallet_success(first):
|
|
|
|
|
+ return True, "购买成功(无需确认)"
|
|
|
|
|
+ if is_rate_limited(status, raw, first):
|
|
|
|
|
+ wait_s = min(5 * attempt, 30)
|
|
|
|
|
+ log("3-buy", f"HTTP {status} 限流,{wait_s}s 后重试 ({attempt}/{buy_retries})")
|
|
|
|
|
+ if attempt < buy_retries:
|
|
|
|
|
+ time.sleep(wait_s)
|
|
|
|
|
+ continue
|
|
|
|
|
+ return False, f"购买被限流(HTTP {status}),请换代理或稍后重试: {clip(raw)}"
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ need = False
|
|
|
|
|
+ if isinstance(first, dict):
|
|
|
|
|
+ need = first.get("need_confirmation") is True or first.get("need_confirmation") == "true"
|
|
|
|
|
+ # 客户端也接受 HTTP 406 + need_confirmation
|
|
|
|
|
+ if need or (status == 406 and "need_confirmation" in raw and "true" in raw):
|
|
|
|
|
+ confirmation_id = extract_confirmation_id(first)
|
|
|
|
|
+ if not confirmation_id:
|
|
|
|
|
+ return False, "需要确认但未返回 confirmation_id"
|
|
|
|
|
+
|
|
|
|
|
+ log("3-buy", f"需要确认 confirmation_id={confirmation_id}")
|
|
|
|
|
+
|
|
|
|
|
+ # 可选:列出当前待确认,便于对照 id / creator_id(确认不走购买代理)
|
|
|
|
|
+ try:
|
|
|
|
|
+ confs = list_confirmations(api_base, session, proxy_url=None)
|
|
|
|
|
+ log("confirm-list", "当前待确认列表", confs)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ log("confirm-list", f"列确认失败(忽略): {e}")
|
|
|
|
|
+
|
|
|
|
|
+ ok = False
|
|
|
|
|
+ try:
|
|
|
|
|
+ ok = operate_confirmation(
|
|
|
|
|
+ api_base, session, confirmation_id, max_retries=confirm_retries, proxy_url=None
|
|
|
|
|
+ )
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ log("4-confirm", f"连续失败,仍尝试 buy2: {e}")
|
|
|
|
|
+ ok = False
|
|
|
|
|
+ if not ok:
|
|
|
|
|
+ log("4-confirm", "确认返回失败/未成功,仍继续 buy2(与客户端一致)")
|
|
|
|
|
+
|
|
|
|
|
+ last_status, last_raw, last_parsed = status, raw, first
|
|
|
|
|
+ for attempt in range(1, buy2_retries + 1):
|
|
|
|
|
+ status2, raw2, second = post_buylisting(
|
|
|
|
|
+ session,
|
|
|
|
|
+ listing_id,
|
|
|
|
|
+ referer,
|
|
|
|
|
+ subtotal,
|
|
|
|
|
+ fee,
|
|
|
|
|
+ total,
|
|
|
|
|
+ confirmation_id,
|
|
|
|
|
+ is_us_account,
|
|
|
|
|
+ f"5-buy2#{attempt}",
|
|
|
|
|
+ proxy_url=proxy_url,
|
|
|
|
|
+ )
|
|
|
|
|
+ last_status, last_raw, last_parsed = status2, raw2, second
|
|
|
|
|
+ if is_rate_limited(status2, raw2, second):
|
|
|
|
|
+ wait_s = min(5 * attempt, 30)
|
|
|
|
|
+ log("5-buy2", f"HTTP {status2} 限流,{wait_s}s 后重试 ({attempt}/{buy2_retries})")
|
|
|
|
|
+ if attempt < buy2_retries:
|
|
|
|
|
+ time.sleep(wait_s)
|
|
|
|
|
+ continue
|
|
|
|
|
+ return False, f"确认后购买被限流(HTTP {status2}): {clip(raw2)}"
|
|
|
|
|
+ if status2 == 406 and isinstance(second, dict) and second.get("need_confirmation") is True:
|
|
|
|
|
+ # 客户端 buy2 遇到再次 need_confirmation 会直接返回,不继续重试该分支
|
|
|
|
|
+ return False, f"buy2 仍返回 need_confirmation: {clip(raw2)}"
|
|
|
|
|
+ if wallet_success(second):
|
|
|
|
|
+ return True, "购买成功(确认后完成)"
|
|
|
|
|
+ if attempt < buy2_retries:
|
|
|
|
|
+ time.sleep(3)
|
|
|
|
|
+
|
|
|
|
|
+ return False, f"确认后购买失败: HTTP {last_status} {clip(last_raw)}"
|
|
|
|
|
+
|
|
|
|
|
+ return False, f"购买失败: HTTP {status} {clip(raw)}"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def resolve_mafile_path(mafile: str) -> str:
|
|
|
|
|
+ """相对路径按本脚本目录解析;已是绝对路径则原样返回。"""
|
|
|
|
|
+ raw = (mafile or "").strip()
|
|
|
|
|
+ if not raw:
|
|
|
|
|
+ return raw
|
|
|
|
|
+ p = Path(raw)
|
|
|
|
|
+ if not p.is_absolute():
|
|
|
|
|
+ p = ROOT_DIR / p
|
|
|
|
|
+ return str(p.resolve())
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def load_config(path: Optional[str]) -> Dict[str, Any]:
|
|
|
|
|
+ cfg_path = Path(path) if path else DEFAULT_CONFIG
|
|
|
|
|
+ if not cfg_path.is_absolute():
|
|
|
|
|
+ cfg_path = ROOT_DIR / cfg_path
|
|
|
|
|
+ if not cfg_path.is_file():
|
|
|
|
|
+ return {}
|
|
|
|
|
+ with cfg_path.open("r", encoding="utf-8") as f:
|
|
|
|
|
+ data = json.load(f)
|
|
|
|
|
+ if not isinstance(data, dict):
|
|
|
|
|
+ raise RuntimeError(f"配置文件格式错误: {cfg_path}")
|
|
|
|
|
+ return data
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def apply_config_defaults(args: argparse.Namespace, cfg: Dict[str, Any]) -> None:
|
|
|
|
|
+ """命令行未显式传入时,用 config.json 补齐。"""
|
|
|
|
|
+ def _set_if_empty(name: str, cfg_key: str, transform=None) -> None:
|
|
|
|
|
+ cur = getattr(args, name, None)
|
|
|
|
|
+ empty = cur is None or cur == "" or cur == 0 or cur is False
|
|
|
|
|
+ # bool / store_true 不走这里
|
|
|
|
|
+ if name in ("us_account", "no_us_account", "confirm_only", "list_conf_only"):
|
|
|
|
|
+ return
|
|
|
|
|
+ if name in ("subtotal", "steam_fee", "pub_fee", "confirm_retries", "buy2_retries"):
|
|
|
|
|
+ if cur not in (None, 0):
|
|
|
|
|
+ return
|
|
|
|
|
+ elif not empty and cur is not None:
|
|
|
|
|
+ # 字符串类:非空则保留 CLI
|
|
|
|
|
+ if isinstance(cur, str) and cur.strip() != "":
|
|
|
|
|
+ return
|
|
|
|
|
+ if not isinstance(cur, str) and cur:
|
|
|
|
|
+ return
|
|
|
|
|
+ if cfg_key not in cfg:
|
|
|
|
|
+ return
|
|
|
|
|
+ val = cfg.get(cfg_key)
|
|
|
|
|
+ if val is None or val == "":
|
|
|
|
|
+ return
|
|
|
|
|
+ if transform:
|
|
|
|
|
+ val = transform(val)
|
|
|
|
|
+ setattr(args, name, val)
|
|
|
|
|
+
|
|
|
|
|
+ _set_if_empty("api_base", "api_base")
|
|
|
|
|
+ _set_if_empty("username", "username")
|
|
|
|
|
+ _set_if_empty("password", "password")
|
|
|
|
|
+ _set_if_empty("mafile", "mafile")
|
|
|
|
|
+ _set_if_empty("listing_id", "listing_id", lambda v: str(v))
|
|
|
|
|
+ _set_if_empty("market_url", "market_url")
|
|
|
|
|
+ _set_if_empty("subtotal", "subtotal", int)
|
|
|
|
|
+ _set_if_empty("steam_fee", "steam_fee", int)
|
|
|
|
|
+ _set_if_empty("pub_fee", "pub_fee", int)
|
|
|
|
|
+ _set_if_empty("proxy", "proxy")
|
|
|
|
|
+ if not args.us_account and not args.no_us_account and bool(cfg.get("is_us_account")):
|
|
|
|
|
+ args.us_account = True
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def build_arg_parser() -> argparse.ArgumentParser:
|
|
|
|
|
+ p = argparse.ArgumentParser(
|
|
|
|
|
+ description="测试 client-tauri Steam 市场购买 + 令牌确认流程"
|
|
|
|
|
+ )
|
|
|
|
|
+ p.add_argument(
|
|
|
|
|
+ "--config",
|
|
|
|
|
+ default=str(DEFAULT_CONFIG),
|
|
|
|
|
+ help=f"配置文件路径(默认 {DEFAULT_CONFIG.name})",
|
|
|
|
|
+ )
|
|
|
|
|
+ p.add_argument("--api-base", default="", help="本地 steamAPI 地址")
|
|
|
|
|
+ p.add_argument("--username", default="", help="Steam 账号(可写在 config.json)")
|
|
|
|
|
+ p.add_argument("--password", default="", help="Steam 密码(可写在 config.json)")
|
|
|
|
|
+ p.add_argument("--mafile", default="", help=".maFile 路径,相对 tljb 目录或绝对路径")
|
|
|
|
|
+ p.add_argument("--listing-id", default="", help="Steam market listing id(购买目标)")
|
|
|
|
|
+ p.add_argument(
|
|
|
|
|
+ "--market-url",
|
|
|
|
|
+ default="",
|
|
|
|
|
+ help="市场物品页 URL,用于 Referer 与自动取价,如 https://steamcommunity.com/market/listings/730/xxx",
|
|
|
|
|
+ )
|
|
|
|
|
+ p.add_argument("--subtotal", type=int, default=0, help="converted_price(美分),不传则从 render 取")
|
|
|
|
|
+ p.add_argument("--steam-fee", type=int, default=0, help="converted_steam_fee(美分)")
|
|
|
|
|
+ p.add_argument("--pub-fee", type=int, default=0, help="converted_publisher_fee(美分)")
|
|
|
|
|
+ p.add_argument(
|
|
|
|
|
+ "--us-account",
|
|
|
|
|
+ action="store_true",
|
|
|
|
|
+ help="美区账号:附带随机账单地址(默认关闭;浏览器非美区仅传空 billing_state)",
|
|
|
|
|
+ )
|
|
|
|
|
+ p.add_argument(
|
|
|
|
|
+ "--no-us-account",
|
|
|
|
|
+ action="store_true",
|
|
|
|
|
+ help="非美区账号(默认行为,可省略)",
|
|
|
|
|
+ )
|
|
|
|
|
+ p.add_argument(
|
|
|
|
|
+ "--proxy",
|
|
|
|
|
+ default="",
|
|
|
|
|
+ help="仅用于市场取价/购买(buylisting);登录与令牌确认不走代理。格式 host:port,如 154.48.255.25:13465",
|
|
|
|
|
+ )
|
|
|
|
|
+ p.add_argument("--confirm-retries", type=int, default=10)
|
|
|
|
|
+ p.add_argument("--buy2-retries", type=int, default=10)
|
|
|
|
|
+ p.add_argument(
|
|
|
|
|
+ "--confirm-only",
|
|
|
|
|
+ action="store_true",
|
|
|
|
|
+ help="只测令牌确认,不买东西(需 --confirmation-id)",
|
|
|
|
|
+ )
|
|
|
|
|
+ p.add_argument("--confirmation-id", default="", help="confirm-only 时使用")
|
|
|
|
|
+ p.add_argument(
|
|
|
|
|
+ "--list-conf-only",
|
|
|
|
|
+ action="store_true",
|
|
|
|
|
+ help="登录后只列出待确认项",
|
|
|
|
|
+ )
|
|
|
|
|
+ return p
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def main() -> int:
|
|
|
|
|
+ args = build_arg_parser().parse_args()
|
|
|
|
|
+ cfg = load_config(args.config)
|
|
|
|
|
+ apply_config_defaults(args, cfg)
|
|
|
|
|
+
|
|
|
|
|
+ if not args.api_base:
|
|
|
|
|
+ args.api_base = STEAM_API_BASE_DEFAULT
|
|
|
|
|
+ if not args.username or not args.password or not args.mafile:
|
|
|
|
|
+ print(
|
|
|
|
|
+ "缺少账号信息:请在 config.json 填写 username/password/mafile,或通过命令行传入",
|
|
|
|
|
+ file=sys.stderr,
|
|
|
|
|
+ )
|
|
|
|
|
+ return 2
|
|
|
|
|
+
|
|
|
|
|
+ args.mafile = resolve_mafile_path(args.mafile)
|
|
|
|
|
+ if not Path(args.mafile).is_file():
|
|
|
|
|
+ print(f"maFile 不存在: {args.mafile}", file=sys.stderr)
|
|
|
|
|
+ return 2
|
|
|
|
|
+
|
|
|
|
|
+ # 默认非美区(与浏览器一致:仅 billing_state 空);显式 --us-account 才带随机地址
|
|
|
|
|
+ is_us = bool(args.us_account) and not args.no_us_account
|
|
|
|
|
+ proxy = normalize_proxy(args.proxy)
|
|
|
|
|
+ if proxy:
|
|
|
|
|
+ log("proxy", f"仅购买/取价走代理: {proxy}(登录与确认直连)")
|
|
|
|
|
+
|
|
|
|
|
+ # 健康检查 steamAPI
|
|
|
|
|
+ try:
|
|
|
|
|
+ requests.get(args.api_base, timeout=3)
|
|
|
|
|
+ except requests.RequestException:
|
|
|
|
|
+ # MapGet 可能没有 /,再试任意端口连通
|
|
|
|
|
+ pass
|
|
|
|
|
+ try:
|
|
|
|
|
+ r = requests.post(f"{args.api_base.rstrip('/')}/wallet-balance", json={}, timeout=5)
|
|
|
|
|
+ _ = r.status_code # 只要能连上即可
|
|
|
|
|
+ except requests.RequestException as e:
|
|
|
|
|
+ print(
|
|
|
|
|
+ f"无法连接 steamAPI ({args.api_base}),请确认本机 5000 端口服务已启动。\n"
|
|
|
|
|
+ f"错误: {e}",
|
|
|
|
|
+ file=sys.stderr,
|
|
|
|
|
+ )
|
|
|
|
|
+ return 2
|
|
|
|
|
+
|
|
|
|
|
+ # 登录不走代理
|
|
|
|
|
+ session = steam_api_login(
|
|
|
|
|
+ args.api_base, args.username, args.password, args.mafile, proxy_url=None
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if args.list_conf_only:
|
|
|
|
|
+ confs = list_confirmations(args.api_base, session, proxy_url=None)
|
|
|
|
|
+ log("DONE", f"待确认 {len(confs)} 条", confs)
|
|
|
|
|
+ return 0
|
|
|
|
|
+
|
|
|
|
|
+ if args.confirm_only:
|
|
|
|
|
+ if not args.confirmation_id:
|
|
|
|
|
+ print("--confirm-only 需要 --confirmation-id", file=sys.stderr)
|
|
|
|
|
+ return 2
|
|
|
|
|
+ confs = list_confirmations(args.api_base, session, proxy_url=None)
|
|
|
|
|
+ log("confirm-list", "确认前列表", confs)
|
|
|
|
|
+ ok = operate_confirmation(
|
|
|
|
|
+ args.api_base, session, args.confirmation_id, args.confirm_retries, proxy_url=None
|
|
|
|
|
+ )
|
|
|
|
|
+ log("DONE", "确认结果", {"success": ok})
|
|
|
|
|
+ return 0 if ok else 1
|
|
|
|
|
+
|
|
|
|
|
+ if not args.listing_id:
|
|
|
|
|
+ print("购买模式需要 --listing-id", file=sys.stderr)
|
|
|
|
|
+ return 2
|
|
|
|
|
+
|
|
|
|
|
+ subtotal, steam_fee, pub_fee = args.subtotal, args.steam_fee, args.pub_fee
|
|
|
|
|
+ referer = args.market_url or "https://steamcommunity.com/market/"
|
|
|
|
|
+ if subtotal <= 0:
|
|
|
|
|
+ if not args.market_url:
|
|
|
|
|
+ print("未传 --subtotal 时必须提供 --market-url 以便 render 取价", file=sys.stderr)
|
|
|
|
|
+ return 2
|
|
|
|
|
+ subtotal, steam_fee, pub_fee = fetch_listing_price(
|
|
|
|
|
+ args.market_url, args.listing_id, session, proxy_url=proxy
|
|
|
|
|
+ )
|
|
|
|
|
+ referer = args.market_url.split("/render")[0]
|
|
|
|
|
+
|
|
|
|
|
+ ok, msg = purchase_one_listing(
|
|
|
|
|
+ api_base=args.api_base,
|
|
|
|
|
+ session=session,
|
|
|
|
|
+ listing_id=str(args.listing_id),
|
|
|
|
|
+ referer=referer,
|
|
|
|
|
+ subtotal=subtotal,
|
|
|
|
|
+ steam_fee=steam_fee,
|
|
|
|
|
+ pub_fee=pub_fee,
|
|
|
|
|
+ is_us_account=is_us,
|
|
|
|
|
+ confirm_retries=args.confirm_retries,
|
|
|
|
|
+ buy2_retries=args.buy2_retries,
|
|
|
|
|
+ proxy_url=proxy,
|
|
|
|
|
+ )
|
|
|
|
|
+ log("DONE", msg, {"success": ok})
|
|
|
|
|
+ return 0 if ok else 1
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ # Windows 控制台 UTF-8
|
|
|
|
|
+ try:
|
|
|
|
|
+ sys.stdout.reconfigure(encoding="utf-8")
|
|
|
|
|
+ sys.stderr.reconfigure(encoding="utf-8")
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+ sys.exit(main())
|