Add files via upload
This commit is contained in:
BIN
mitmproxy/addons/__pycache__/mitm_xbox_addon.cpython-314.pyc
Normal file
BIN
mitmproxy/addons/__pycache__/mitm_xbox_addon.cpython-314.pyc
Normal file
Binary file not shown.
732
mitmproxy/addons/mitm_xbox_addon.py
Normal file
732
mitmproxy/addons/mitm_xbox_addon.py
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from mitmproxy import http, ctx
|
||||||
|
import json, re, os, time, uuid, ssl
|
||||||
|
from urllib import request as urlrequest
|
||||||
|
from urllib.parse import urlparse, parse_qs, unquote, urlencode, urlunparse
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Telegram 推送配置
|
||||||
|
# =========================
|
||||||
|
TELEGRAM_BOT_TOKEN = "8293676109:AAG3f6GnZNxJiwxwyGh_OtTU4wGn6-ypg_4"
|
||||||
|
TELEGRAM_CHAT_ID = "1732587552" # 发送到你的用户ID
|
||||||
|
TELEGRAM_API_BASE = "https://api.telegram.org"
|
||||||
|
|
||||||
|
def _tg_send(text: str):
|
||||||
|
"""
|
||||||
|
主动向 Telegram 发送一条消息。
|
||||||
|
disable_notification=False -> 强制“正常通知”(不要静默)。
|
||||||
|
即使失败也不会中断 mitmproxy,只写日志。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"chat_id": TELEGRAM_CHAT_ID,
|
||||||
|
"text": text,
|
||||||
|
"parse_mode": "HTML", # 可以放简单加粗/表情
|
||||||
|
"disable_web_page_preview": True,
|
||||||
|
"disable_notification": False # 非静默,尽量触发弹窗
|
||||||
|
}
|
||||||
|
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||||
|
req = urlrequest.Request(
|
||||||
|
f"{TELEGRAM_API_BASE}/bot{TELEGRAM_BOT_TOKEN}/sendMessage",
|
||||||
|
data=data,
|
||||||
|
method="POST"
|
||||||
|
)
|
||||||
|
req.add_header("Content-Type", "application/json")
|
||||||
|
# 走 HTTPS
|
||||||
|
ctx_ssl = ssl.create_default_context()
|
||||||
|
with urlrequest.urlopen(req, context=ctx_ssl, timeout=10) as resp:
|
||||||
|
# 读一下结果防止连接挂着
|
||||||
|
_ = resp.read()
|
||||||
|
ctx.log.info("[tg] sent notification ok")
|
||||||
|
except Exception as ex:
|
||||||
|
ctx.log.warn(f"[tg] send failed: {ex}")
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 状态持久化 (state.json)
|
||||||
|
# =========================
|
||||||
|
DEFAULT_STATE_PATHS = [
|
||||||
|
"/home/mitmproxy/.mitmproxy/state.json",
|
||||||
|
"/opt/mitmproxy/state.json",
|
||||||
|
"/data/mitmproxy/state.json",
|
||||||
|
os.path.join(os.path.dirname(__file__), "state.json"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _pick_state_path():
|
||||||
|
for p in DEFAULT_STATE_PATHS:
|
||||||
|
d = os.path.dirname(p)
|
||||||
|
try:
|
||||||
|
if d and not os.path.exists(d):
|
||||||
|
os.makedirs(d, exist_ok=True)
|
||||||
|
return p
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return "state.json"
|
||||||
|
|
||||||
|
STATE_PATH = _pick_state_path()
|
||||||
|
|
||||||
|
def _load_state():
|
||||||
|
try:
|
||||||
|
with open(STATE_PATH, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_state(st):
|
||||||
|
tmp = STATE_PATH + ".tmp"
|
||||||
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(st, f, ensure_ascii=False, indent=2, sort_keys=True)
|
||||||
|
os.replace(tmp, STATE_PATH)
|
||||||
|
|
||||||
|
def getv(key, default=""):
|
||||||
|
st = _load_state()
|
||||||
|
return st.get(key, default)
|
||||||
|
|
||||||
|
def setv(key, value):
|
||||||
|
st = _load_state()
|
||||||
|
st[key] = value
|
||||||
|
_save_state(st)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 三元组存取 (ProductId, SkuId, AvailabilityId)
|
||||||
|
# =========================
|
||||||
|
_PRODUCTS_KEY = "__xbox_products__" # list[ {ProductId,SkuId,AvailabilityId} ]
|
||||||
|
_LISTSTR_KEY = "XboxProductList" # "product1=pid|sku|aid;product2=..."
|
||||||
|
|
||||||
|
def _get_products():
|
||||||
|
data = getv(_PRODUCTS_KEY, [])
|
||||||
|
if not isinstance(data, list):
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for it in data:
|
||||||
|
try:
|
||||||
|
p = str(it.get("ProductId", "")).strip()
|
||||||
|
s = str(it.get("SkuId", "")).strip()
|
||||||
|
a = str(it.get("AvailabilityId", "")).strip()
|
||||||
|
if p and s and a:
|
||||||
|
out.append({"ProductId": p, "SkuId": s, "AvailabilityId": a})
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _save_products(lst):
|
||||||
|
"""
|
||||||
|
- 去重
|
||||||
|
- 回写到 state.json (__xbox_products__ / XboxProductList)
|
||||||
|
- 如果 XboxProductList 有变化 -> 主动 Telegram 推送
|
||||||
|
"""
|
||||||
|
# 之前的 product list 串,等会对比判断是否更新
|
||||||
|
old_list_str = getv(_LISTSTR_KEY, "")
|
||||||
|
|
||||||
|
# 去重
|
||||||
|
seen = set()
|
||||||
|
uniq = []
|
||||||
|
for it in lst:
|
||||||
|
key = f"{it['ProductId']}||{it['SkuId']}||{it['AvailabilityId']}"
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
uniq.append(it)
|
||||||
|
|
||||||
|
# 写入内存状态
|
||||||
|
setv(_PRODUCTS_KEY, uniq)
|
||||||
|
|
||||||
|
# 同步生成 XboxProductList 串
|
||||||
|
parts = []
|
||||||
|
for i, it in enumerate(uniq, 1):
|
||||||
|
parts.append(f"product{i}={it['ProductId']}|{it['SkuId']}|{it['AvailabilityId']}")
|
||||||
|
new_list_str = ";".join(parts)
|
||||||
|
setv(_LISTSTR_KEY, new_list_str)
|
||||||
|
|
||||||
|
# 如果产品列表字符串发生变化 -> 发通知
|
||||||
|
if new_list_str != old_list_str:
|
||||||
|
count_now = len(uniq)
|
||||||
|
_tg_send(f"🔔 XboxProductList 更新\n当前产品数: {count_now}")
|
||||||
|
|
||||||
|
def _add_product(pid, sid, aid):
|
||||||
|
if not (pid and sid and aid):
|
||||||
|
return False
|
||||||
|
cur = _get_products()
|
||||||
|
key = f"{pid}||{sid}||{aid}"
|
||||||
|
if any((x["ProductId"]+"||"+x["SkuId"]+"||"+x["AvailabilityId"]) == key for x in cur):
|
||||||
|
return False
|
||||||
|
cur.append({"ProductId": pid, "SkuId": sid, "AvailabilityId": aid})
|
||||||
|
_save_products(cur)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# HTTP JSON helper
|
||||||
|
# =========================
|
||||||
|
def _json_response(flow: http.HTTPFlow, data: dict, status: int = 200):
|
||||||
|
body = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8")
|
||||||
|
flow.response = http.Response.make(
|
||||||
|
status,
|
||||||
|
body,
|
||||||
|
{"Content-Type": "application/json; charset=utf-8"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 抽三元组的递归解析
|
||||||
|
# =========================
|
||||||
|
def _extract_triples_from_json_like(text: str):
|
||||||
|
out = set()
|
||||||
|
if not text:
|
||||||
|
return out
|
||||||
|
data = None
|
||||||
|
try:
|
||||||
|
data = json.loads(text)
|
||||||
|
except Exception:
|
||||||
|
# 某些接口会返回 ")]}',\n{json...}" 之类,尝试抓第一个 {...} 或 [...]
|
||||||
|
m = re.search(r"(\{[\s\S]*\}|\[[\s\S]*\])", text)
|
||||||
|
if m:
|
||||||
|
try:
|
||||||
|
data = json.loads(m.group(1))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if data is None:
|
||||||
|
return out
|
||||||
|
|
||||||
|
def is_obj(v):
|
||||||
|
return isinstance(v, dict)
|
||||||
|
|
||||||
|
def visit(node):
|
||||||
|
if isinstance(node, list):
|
||||||
|
for v in node:
|
||||||
|
visit(v)
|
||||||
|
return
|
||||||
|
if not is_obj(node):
|
||||||
|
return
|
||||||
|
at = node.get("actionType")
|
||||||
|
if isinstance(at, str) and at.lower() == "cart":
|
||||||
|
args = node.get("actionArguments", {})
|
||||||
|
if is_obj(args):
|
||||||
|
p = str(args.get("ProductId", "")).strip()
|
||||||
|
s = str(args.get("SkuId", "")).strip()
|
||||||
|
a = str(args.get("AvailabilityId", "")).strip()
|
||||||
|
if p and s and a:
|
||||||
|
out.add((p, s, a))
|
||||||
|
for v in node.values():
|
||||||
|
visit(v)
|
||||||
|
|
||||||
|
visit(data)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# PUT /cart/loadCart 调用工具 (加购)
|
||||||
|
# =========================
|
||||||
|
UA = (
|
||||||
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_3_1 like Mac OS X) "
|
||||||
|
"AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/133.0.3065.54 "
|
||||||
|
"Version/18.0 Mobile/15E148 Safari/604.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
CART_API_URL = (
|
||||||
|
"https://cart.production.store-web.dynamics.com/"
|
||||||
|
"cart/v1.0/cart/loadCart?cartType=consumer&appId=StoreWeb"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _cart_put_single(muid: str, ms_cv: str, pid: str, sid: str, aid: str, x_vec: str = ""):
|
||||||
|
"""
|
||||||
|
向微软的购物车接口发一次 PUT, 把 (ProductId, SkuId, AvailabilityId) 放进去。
|
||||||
|
"""
|
||||||
|
payload = {
|
||||||
|
"locale": "en-ng",
|
||||||
|
"market": "NG",
|
||||||
|
"catalogClientType": "storeWeb",
|
||||||
|
"friendlyName": "cart-NG",
|
||||||
|
"riskSessionId": str(uuid.uuid4()),
|
||||||
|
"clientContext": {"client": "UniversalWebStore.Cart", "deviceType": "Pc"},
|
||||||
|
"itemsToAdd": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"productId": pid,
|
||||||
|
"skuId": sid,
|
||||||
|
"availabilityId": aid,
|
||||||
|
"campaignId": "xboxcomct",
|
||||||
|
"quantity": 1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
||||||
|
req = urlrequest.Request(CART_API_URL, data=data, method="PUT")
|
||||||
|
req.add_header("content-type", "application/json")
|
||||||
|
req.add_header("accept", "*/*")
|
||||||
|
req.add_header("x-authorization-muid", muid or "")
|
||||||
|
req.add_header("x-validation-field-1", "9pgbhbppjf2b")
|
||||||
|
req.add_header("ms-cv", ms_cv or "")
|
||||||
|
if x_vec:
|
||||||
|
req.add_header("x-ms-vector-id", x_vec)
|
||||||
|
req.add_header("accept-language", "en-US,en;q=0.9")
|
||||||
|
req.add_header("accept-encoding", "gzip, deflate, br")
|
||||||
|
req.add_header("sec-fetch-site", "cross-site")
|
||||||
|
req.add_header("sec-fetch-mode", "cors")
|
||||||
|
req.add_header("sec-fetch-dest", "empty")
|
||||||
|
req.add_header("origin", "https://www.microsoft.com")
|
||||||
|
req.add_header("referer", "https://www.microsoft.com/")
|
||||||
|
req.add_header("user-agent", UA)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ctx_ssl = ssl.create_default_context()
|
||||||
|
with urlrequest.urlopen(req, context=ctx_ssl, timeout=20) as resp:
|
||||||
|
return resp.status, (200 <= resp.status < 300), resp.read()[:2048]
|
||||||
|
except Exception as ex:
|
||||||
|
return 0, False, str(ex).encode("utf-8", errors="ignore")
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# 主插件
|
||||||
|
# =========================
|
||||||
|
class MitmXboxAddonNoDashV4:
|
||||||
|
TARGET_IDS = [
|
||||||
|
"9PNTSH5SKCL5",
|
||||||
|
"9nfmccp0pm67",
|
||||||
|
"9npbvj8lwsvn",
|
||||||
|
"9pcgszz8zpq2",
|
||||||
|
"9P54FF0VQD7R",
|
||||||
|
"9NCJZN3LBD3P",
|
||||||
|
]
|
||||||
|
RX_XBOX_PATH = re.compile(r"^/([A-Za-z]{2}-[A-Za-z]{2})(/.*)$")
|
||||||
|
|
||||||
|
def load(self, loader):
|
||||||
|
ctx.log.info(f"[xbox-nodash-v4] State path: {STATE_PATH}")
|
||||||
|
|
||||||
|
# -----------------
|
||||||
|
# 本地管理端点 /__xbox/*
|
||||||
|
# -----------------
|
||||||
|
def _normalize_seg(self, raw_path: str):
|
||||||
|
if not raw_path or not raw_path.lower().startswith("/__xbox/"):
|
||||||
|
return "", ""
|
||||||
|
seg = raw_path[len("/__xbox/") :].split("?")[0]
|
||||||
|
rawseg = seg.strip("/")
|
||||||
|
normseg = re.sub(r"[^a-z0-9]", "", rawseg.lower())
|
||||||
|
return rawseg, normseg
|
||||||
|
|
||||||
|
def _handle_admin(self, flow: http.HTTPFlow):
|
||||||
|
path = flow.request.path or "/"
|
||||||
|
rawseg, seg = self._normalize_seg(path)
|
||||||
|
if not seg:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# /__xbox/check
|
||||||
|
if seg == "check":
|
||||||
|
keys = [
|
||||||
|
"cart-x-authorization-muid",
|
||||||
|
"cart-ms-cv",
|
||||||
|
"cart-x-ms-vector-id",
|
||||||
|
"authorization",
|
||||||
|
"cartId",
|
||||||
|
]
|
||||||
|
state = {
|
||||||
|
k: getv("fiddler.custom." + k, "")
|
||||||
|
if k not in ("authorization", "cartId")
|
||||||
|
else getv(k, "")
|
||||||
|
for k in keys
|
||||||
|
}
|
||||||
|
prods = _get_products()
|
||||||
|
_json_response(
|
||||||
|
flow,
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"state": state,
|
||||||
|
"products_count": len(prods),
|
||||||
|
"XboxProductList": getv(_LISTSTR_KEY, ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# /__xbox/viewproducts
|
||||||
|
if seg == "viewproducts":
|
||||||
|
prods = _get_products()
|
||||||
|
_json_response(
|
||||||
|
flow,
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"count": len(prods),
|
||||||
|
"products": prods,
|
||||||
|
"XboxProductList": getv(_LISTSTR_KEY, ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# /__xbox/clearproducts
|
||||||
|
if seg == "clearproducts":
|
||||||
|
_save_products([]) # 这个里面也会触发Telegram通知(产品数=0)
|
||||||
|
_json_response(flow, {"ok": True, "message": "cleared"})
|
||||||
|
return True
|
||||||
|
|
||||||
|
# /__xbox/addtocart
|
||||||
|
if seg == "addtocart":
|
||||||
|
prods = _get_products()
|
||||||
|
if not prods:
|
||||||
|
_json_response(
|
||||||
|
flow, {"ok": False, "error": "no products captured"}, 400
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
muid = getv("fiddler.custom.cart-x-authorization-muid", "")
|
||||||
|
ms_cv = getv("fiddler.custom.cart-ms-cv", "")
|
||||||
|
x_vec = getv("fiddler.custom.cart-x-ms-vector-id", "")
|
||||||
|
|
||||||
|
if not muid or not ms_cv:
|
||||||
|
_json_response(
|
||||||
|
flow,
|
||||||
|
{
|
||||||
|
"ok": False,
|
||||||
|
"error": "missing cart-x-authorization-muid / cart-ms-cv",
|
||||||
|
},
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
successes, failures = [], []
|
||||||
|
remaining = list(prods)
|
||||||
|
|
||||||
|
for item in prods:
|
||||||
|
pid = item["ProductId"]
|
||||||
|
sid = item["SkuId"]
|
||||||
|
aid = item["AvailabilityId"]
|
||||||
|
id_triple = f"{pid}/{sid}/{aid}"
|
||||||
|
|
||||||
|
code, ok, _ = _cart_put_single(muid, ms_cv, pid, sid, aid, x_vec)
|
||||||
|
if ok:
|
||||||
|
successes.append({"triple": id_triple, "status": code})
|
||||||
|
# 从 remaining 里移除成功的
|
||||||
|
remaining = [
|
||||||
|
x
|
||||||
|
for x in remaining
|
||||||
|
if not (
|
||||||
|
x["ProductId"] == pid
|
||||||
|
and x["SkuId"] == sid
|
||||||
|
and x["AvailabilityId"] == aid
|
||||||
|
)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
failures.append({"triple": id_triple, "status": code or "ERR"})
|
||||||
|
|
||||||
|
time.sleep(0.15)
|
||||||
|
|
||||||
|
# 写回剩下(没成功的还保留),会触发 _save_products 内部逻辑
|
||||||
|
_save_products(remaining)
|
||||||
|
|
||||||
|
_json_response(
|
||||||
|
flow,
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"tried": len(prods),
|
||||||
|
"success": len(successes),
|
||||||
|
"failed": len(failures),
|
||||||
|
"failures": failures,
|
||||||
|
"remaining": len(remaining),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
_json_response(
|
||||||
|
flow,
|
||||||
|
{
|
||||||
|
"ok": False,
|
||||||
|
"error": "unknown admin action",
|
||||||
|
"seen_seg": seg,
|
||||||
|
"raw_path": path,
|
||||||
|
"rawseg": rawseg,
|
||||||
|
},
|
||||||
|
404,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# -----------------
|
||||||
|
# 自动 302 跳转规则
|
||||||
|
# -----------------
|
||||||
|
def _maybe_redirect(self, flow: http.HTTPFlow):
|
||||||
|
fullurl = (flow.request.pretty_url or "").strip()
|
||||||
|
if not fullurl:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# (0) Microsoft Store → Xbox corehalo/<id>
|
||||||
|
# 匹配 https://www.microsoft.com/en-us/store/...ID(12位)...
|
||||||
|
# 跳转到 https://www.xbox.com/en-us/games/store/corehalo/ID
|
||||||
|
try:
|
||||||
|
m_ms = re.search(
|
||||||
|
r"(?i)^https?://(?:www\.)?microsoft\.com/en-us/store/.*?([A-Za-z0-9]{12})(?:[\/?#]|$)",
|
||||||
|
fullurl,
|
||||||
|
)
|
||||||
|
if m_ms:
|
||||||
|
game_id = m_ms.group(1)
|
||||||
|
new_url = "https://www.xbox.com/en-us/games/store/corehalo/" + game_id
|
||||||
|
ctx.log.info(f"[xbox-nodash-v4] Redirect microsoft.com -> {new_url}")
|
||||||
|
html_body = (
|
||||||
|
"<html><head><meta http-equiv='refresh' content='0;url="
|
||||||
|
+ new_url
|
||||||
|
+ "'></head><body>Redirecting to "
|
||||||
|
+ new_url
|
||||||
|
+ "</body></html>"
|
||||||
|
).encode("utf-8", errors="ignore")
|
||||||
|
flow.response = http.Response.make(
|
||||||
|
302,
|
||||||
|
html_body,
|
||||||
|
{
|
||||||
|
"Location": new_url,
|
||||||
|
"Content-Type": "text/html; charset=UTF-8",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
ctx.log.warn(f"[xbox-nodash-v4] ms store redirect error: {e}")
|
||||||
|
|
||||||
|
# 继续原有的重定向逻辑
|
||||||
|
parsed = urlparse(fullurl)
|
||||||
|
host = parsed.hostname or ""
|
||||||
|
path = parsed.path or ""
|
||||||
|
query = parsed.query or ""
|
||||||
|
qd = parse_qs(query, keep_blank_values=True)
|
||||||
|
|
||||||
|
# (1) xbox.com 区域纠正成 en-us
|
||||||
|
if host.lower() == "www.xbox.com":
|
||||||
|
m = self.RX_XBOX_PATH.match(path)
|
||||||
|
if m:
|
||||||
|
region = m.group(1)
|
||||||
|
suffix = m.group(2) or "/"
|
||||||
|
if region.lower() not in {"es-ar", "en-us"}:
|
||||||
|
new = f"https://www.xbox.com/en-us{suffix}"
|
||||||
|
ctx.log.info(f"[xbox-nodash-v4] Redirect xbox.com {fullurl} -> {new}")
|
||||||
|
flow.response = http.Response.make(302, b"", {"Location": new})
|
||||||
|
return True
|
||||||
|
|
||||||
|
# (2) xboxfan.com/goto?region=xx → 改成 en-us
|
||||||
|
if host.lower() == "xboxfan.com" and path.lower().startswith("/goto"):
|
||||||
|
region_vals = qd.get("region", [])
|
||||||
|
if region_vals and region_vals[0].lower() not in {"es-ar", "en-us"}:
|
||||||
|
qd["region"] = ["en-us"]
|
||||||
|
new_q = urlencode(qd, doseq=True)
|
||||||
|
new = urlunparse(("https", host, path, "", new_q, ""))
|
||||||
|
ctx.log.info(f"[xbox-nodash-v4] Redirect xboxfan {fullurl} -> {new}")
|
||||||
|
flow.response = http.Response.make(302, b"", {"Location": new})
|
||||||
|
return True
|
||||||
|
|
||||||
|
# (3) app.corehalo.com/ms/link/go?r=xx → 改成 en-us
|
||||||
|
if host.lower() == "app.corehalo.com" and path.lower().startswith("/ms/link/go"):
|
||||||
|
r_vals = qd.get("r", [])
|
||||||
|
if r_vals and r_vals[0].lower() not in {"es-ar", "en-us"}:
|
||||||
|
qd["r"] = ["en-us"]
|
||||||
|
new_q = urlencode(qd, doseq=True)
|
||||||
|
new = urlunparse(("https", host, path, "", new_q, ""))
|
||||||
|
ctx.log.info(f"[xbox-nodash-v4] Redirect corehalo {fullurl} -> {new}")
|
||||||
|
flow.response = http.Response.make(302, b"", {"Location": new})
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# -----------------
|
||||||
|
# request 钩子
|
||||||
|
# -----------------
|
||||||
|
def request(self, flow: http.HTTPFlow):
|
||||||
|
# 先跑跳转
|
||||||
|
try:
|
||||||
|
if self._maybe_redirect(flow):
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
ctx.log.warn(f"[xbox-nodash-v4] redirect check error: {e}")
|
||||||
|
|
||||||
|
# 管理端点
|
||||||
|
if self._handle_admin(flow):
|
||||||
|
return
|
||||||
|
|
||||||
|
# 拦截并修补 RequestParentalApproval:只改 Authorization 和 cartId
|
||||||
|
if (
|
||||||
|
flow.request.host == "buynow.production.store-web.dynamics.com"
|
||||||
|
and flow.request.method == "POST"
|
||||||
|
and "/v1.0/Cart/RequestParentalApproval" in flow.request.path
|
||||||
|
):
|
||||||
|
cart_id_stored = getv("cartId", "")
|
||||||
|
auth_stored = getv("authorization", "")
|
||||||
|
if not cart_id_stored or not auth_stored:
|
||||||
|
ctx.log.error("[xbox-nodash-v4] 缺少存储的 cartId 或 authorization!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 覆盖 Authorization 头
|
||||||
|
flow.request.headers["Authorization"] = auth_stored
|
||||||
|
|
||||||
|
# 覆盖请求体里的 cartId
|
||||||
|
try:
|
||||||
|
body = flow.request.get_text() or ""
|
||||||
|
if body:
|
||||||
|
new_body = re.sub(
|
||||||
|
r'"cartId"\s*:\s*"[^"]*"',
|
||||||
|
f'"cartId":"{cart_id_stored}"',
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
if new_body != body:
|
||||||
|
flow.request.set_text(new_body)
|
||||||
|
ctx.log.info("[xbox-nodash-v4] 替换 cartId")
|
||||||
|
else:
|
||||||
|
ctx.log.warn("[xbox-nodash-v4] 未发现 cartId 字段,仅替换 Authorization")
|
||||||
|
except Exception as ex:
|
||||||
|
ctx.log.warn(f"[xbox-nodash-v4] 替换 cartId 失败: {ex}")
|
||||||
|
|
||||||
|
host = flow.request.host or ""
|
||||||
|
path = flow.request.path or ""
|
||||||
|
fullurl = flow.request.pretty_url or ""
|
||||||
|
|
||||||
|
# family 请求拦截
|
||||||
|
if (
|
||||||
|
host == "account.microsoft.com"
|
||||||
|
and flow.request.method == "POST"
|
||||||
|
and path.startswith("/family/api/buy/requests/complete")
|
||||||
|
):
|
||||||
|
body_low = (flow.request.get_text() or "").lower()
|
||||||
|
for tid in self.TARGET_IDS:
|
||||||
|
if tid.lower() in body_low:
|
||||||
|
flow.response = http.Response.make(
|
||||||
|
403,
|
||||||
|
b"Blocked by mitmproxy rule",
|
||||||
|
{"Content-Type": "text/plain; charset=utf-8"},
|
||||||
|
)
|
||||||
|
ctx.log.info(f"[xbox-nodash-v4] Blocked family complete for productId: {tid}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 购物车 body 规范化(强制 NG / en-ng / cart-NG)
|
||||||
|
if host.endswith("store-web.dynamics.com") and "/v1.0/cart" in path:
|
||||||
|
try:
|
||||||
|
txt = flow.request.get_text() or ""
|
||||||
|
if not txt.strip():
|
||||||
|
return
|
||||||
|
modified = False
|
||||||
|
try:
|
||||||
|
data = json.loads(txt)
|
||||||
|
mk = data.get("market")
|
||||||
|
lc = data.get("locale")
|
||||||
|
fn = data.get("friendlyName")
|
||||||
|
if mk and mk.upper() != "AR":
|
||||||
|
data["market"] = "NG"; modified = True
|
||||||
|
if lc and lc.lower() != "es-ar":
|
||||||
|
data["locale"] = "en-ng"; modified = True
|
||||||
|
if fn and fn != "cart-AR":
|
||||||
|
data["friendlyName"] = "cart-NG"; modified = True
|
||||||
|
if modified:
|
||||||
|
flow.request.set_text(json.dumps(data, ensure_ascii=False))
|
||||||
|
ctx.log.info("[xbox-nodash-v4] cart body normalized to NG")
|
||||||
|
except Exception:
|
||||||
|
new = re.sub(r'"market"\s*:\s*"(?!ar|AR)\w{2}"', r'"market":"NG"', txt)
|
||||||
|
new = re.sub(r'"locale"\s*:\s*"(?!es-ar|es-AR)[\w-]+"', r'"locale":"en-NG"', new)
|
||||||
|
new = re.sub(r'"friendlyName"\s*:\s*"(?!cart-AR)cart-\w{2}"', r'"friendlyName":"cart-NG"', new)
|
||||||
|
if new != txt:
|
||||||
|
flow.request.set_text(new)
|
||||||
|
ctx.log.info("[xbox-nodash-v4] cart body normalized by regex")
|
||||||
|
except Exception as ex:
|
||||||
|
ctx.log.warn(f"[xbox-nodash-v4] cart normalization failed: {ex}")
|
||||||
|
|
||||||
|
# eligibilityCheck:抓 Authorization 和 cartId
|
||||||
|
if (
|
||||||
|
flow.request.method.upper() == "PUT"
|
||||||
|
and "eligibilitycheck" in path.lower()
|
||||||
|
and "cart.production.store-web.dynamics.com" in host
|
||||||
|
):
|
||||||
|
# 授权头直接存
|
||||||
|
auth = flow.request.headers.get("Authorization", "")
|
||||||
|
if auth:
|
||||||
|
setv("authorization", auth)
|
||||||
|
|
||||||
|
# 抓 cartId(query param 里)
|
||||||
|
try:
|
||||||
|
qs = parse_qs(urlparse(fullurl).query)
|
||||||
|
cart_id = qs.get("cartId", [""])[0] or qs.get("cartid", [""])[0]
|
||||||
|
if cart_id:
|
||||||
|
cart_id = unquote(cart_id)
|
||||||
|
old_cart = getv("cartId", "")
|
||||||
|
if cart_id != old_cart:
|
||||||
|
setv("cartId", cart_id)
|
||||||
|
# cartId 变更 -> 主动推送
|
||||||
|
_tg_send(f"🔔 cartId 更新:\n{cart_id}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ctx.log.info("[xbox-nodash-v4] captured eligibilityCheck params")
|
||||||
|
|
||||||
|
# loadCart:抓购物车请求头
|
||||||
|
if (
|
||||||
|
flow.request.method.upper() == "PUT"
|
||||||
|
and "/v1.0/cart/loadCart" in path
|
||||||
|
and "cart.production.store-web.dynamics.com" in host
|
||||||
|
):
|
||||||
|
for hk, sk in [
|
||||||
|
("X-Authorization-Muid", "fiddler.custom.cart-x-authorization-muid"),
|
||||||
|
("MS-CV", "fiddler.custom.cart-ms-cv"),
|
||||||
|
("X-MS-Vector-Id", "fiddler.custom.cart-x-ms-vector-id"),
|
||||||
|
]:
|
||||||
|
val = flow.request.headers.get(hk)
|
||||||
|
if val:
|
||||||
|
setv(sk, val)
|
||||||
|
ctx.log.info("[xbox-nodash-v4] captured loadCart headers")
|
||||||
|
|
||||||
|
# -----------------
|
||||||
|
# response 钩子
|
||||||
|
# -----------------
|
||||||
|
def response(self, flow: http.HTTPFlow):
|
||||||
|
req, resp = flow.request, flow.response
|
||||||
|
host = (req.host or "").lower()
|
||||||
|
path = (req.path or "").lower()
|
||||||
|
|
||||||
|
# 抓取 productActions(emerald.xboxservices.com),提取三元组 -> _add_product
|
||||||
|
try:
|
||||||
|
if (
|
||||||
|
req.method.upper() == "GET"
|
||||||
|
and host == "emerald.xboxservices.com"
|
||||||
|
and path.startswith("/xboxcomfd/productactions/")
|
||||||
|
):
|
||||||
|
# 我们只要 locale=en-us 的
|
||||||
|
if "locale=en-us" in (req.pretty_url or "").lower():
|
||||||
|
txt = resp.get_text() or ""
|
||||||
|
triples = _extract_triples_from_json_like(txt)
|
||||||
|
if triples:
|
||||||
|
added_cnt = 0
|
||||||
|
for p, s, a in triples:
|
||||||
|
if _add_product(p, s, a):
|
||||||
|
added_cnt += 1
|
||||||
|
ctx.log.info(
|
||||||
|
f"[xbox-nodash-v4] productActions captured: +{added_cnt}, total={len(_get_products())}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
ctx.log.warn(f"[xbox-nodash-v4] capture triples error: {e}")
|
||||||
|
|
||||||
|
# family product endpoint:把 productKind 改成 "Game"
|
||||||
|
try:
|
||||||
|
fullurl = (req.pretty_url or "").strip()
|
||||||
|
if fullurl.lower().startswith(
|
||||||
|
"https://account.microsoft.com/family/api/product?puid="
|
||||||
|
):
|
||||||
|
txt = ""
|
||||||
|
try:
|
||||||
|
txt = resp.get_text()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
txt = resp.content.decode("utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
txt = ""
|
||||||
|
if not txt:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
data = json.loads(txt)
|
||||||
|
except Exception as ex:
|
||||||
|
ctx.log.warn(f"[xbox-nodash-v4] response body not JSON: {ex}")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if "productDocument" not in data or not isinstance(data["productDocument"], dict):
|
||||||
|
data["productDocument"] = {}
|
||||||
|
pd = data["productDocument"]
|
||||||
|
if "product" not in pd or not isinstance(pd["product"], dict):
|
||||||
|
pd["product"] = {}
|
||||||
|
pd["product"]["productKind"] = "Game"
|
||||||
|
resp.set_text(json.dumps(data, ensure_ascii=False))
|
||||||
|
ctx.log.info("[xbox-nodash-v4] Modified productKind -> Game for product endpoint")
|
||||||
|
except Exception as ex:
|
||||||
|
ctx.log.warn(f"[xbox-nodash-v4] failed to set productKind: {ex}")
|
||||||
|
except Exception as e:
|
||||||
|
ctx.log.warn(f"[xbox-nodash-v4] unexpected in response hook: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
addons = [MitmXboxAddonNoDashV4()]
|
||||||
65
mitmproxy/docker-compose.yml
Normal file
65
mitmproxy/docker-compose.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
mitmproxy:
|
||||||
|
image: mitmproxy/mitmproxy:latest
|
||||||
|
container_name: mitmproxy
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "31280:8080" # HTTP/HTTPS 代理入口 (客户端走这个端口当代理)
|
||||||
|
- "18081:8081" # mitmweb 控制台(带密码)
|
||||||
|
|
||||||
|
command:
|
||||||
|
- mitmweb
|
||||||
|
- "--listen-host"
|
||||||
|
- "0.0.0.0"
|
||||||
|
- "--web-host"
|
||||||
|
- "0.0.0.0"
|
||||||
|
- "--set"
|
||||||
|
- "web_password=8130899"
|
||||||
|
- "--set"
|
||||||
|
- "block_global=false" # 允许外部客户端接入
|
||||||
|
- "--set"
|
||||||
|
- "connection_strategy=lazy"
|
||||||
|
- "--set"
|
||||||
|
- "http2=false" # 关 http2,方便改请求头/请求体
|
||||||
|
- "--proxyauth=hbxnlsy:8130899" # 代理验证 用户名:密码
|
||||||
|
- "-s"
|
||||||
|
- "/addons/mitm_xbox_addon.py" # 你的主脚本(含自动发 Telegram 通知)
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- /opt/1panel/docker/compose/mitmproxy/addons:/addons
|
||||||
|
- /opt/1panel/docker/compose/mitmproxy/mitm-data:/home/mitmproxy/.mitmproxy
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
logging:
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
xboxbot:
|
||||||
|
build:
|
||||||
|
context: ./xboxbot
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: xboxbot
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- TELEGRAM_BOT_TOKEN=8293676109:AAG3f6GnZNxJiwxwyGh_OtTU4wGn6-ypg_4
|
||||||
|
- TELEGRAM_ALLOWED_USER_ID=1732587552,7935041828
|
||||||
|
- STATE_PATH=/shared/state.json
|
||||||
|
- MITM_MARKET_LOCALE=en-ng
|
||||||
|
- MITM_MARKET_CODE=NG
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# 让 bot 跟 mitmproxy 共享同一份 state.json
|
||||||
|
- /opt/1panel/docker/compose/mitmproxy/mitm-data:/shared:rw
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- mitmproxy
|
||||||
|
|
||||||
|
logging:
|
||||||
|
options:
|
||||||
|
max-size: "5m"
|
||||||
|
max-file: "3"
|
||||||
20
mitmproxy/mitm-data/mitmproxy-ca-cert.cer
Normal file
20
mitmproxy/mitm-data/mitmproxy-ca-cert.cer
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDNTCCAh2gAwIBAgIUd4CmNZbEMWsQ9UFIrXFJx87zufEwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwKDESMBAGA1UEAwwJbWl0bXByb3h5MRIwEAYDVQQKDAltaXRtcHJveHkwHhcN
|
||||||
|
MjUxMDI1MTIzMzA0WhcNMzUxMDI1MTIzMzA0WjAoMRIwEAYDVQQDDAltaXRtcHJv
|
||||||
|
eHkxEjAQBgNVBAoMCW1pdG1wcm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBALCVT6FiAvJgHkPt7lgd7ZfhD5lFmjZ/P7OmBloRZ4A/xTBVTV/a7KGJ
|
||||||
|
J7tscBRrTWihNHIQZcm8sKpCgwLrQc4SCTBlOPHQ8Imh9gLe8r1XBu+nCduScYXy
|
||||||
|
31R9Lpyh89RqW/384HgVMVKRf8qcv6qikk5+GqxZxtgIBmR0RyU+jPyBSFV0djjx
|
||||||
|
kdkuKcFF8VNayL2QCXtofBS5/+xk+iLzMBIqU8WIXGEvf2mTq8iM63bXQNVq3Iq/
|
||||||
|
zyUgOlAF++XciY6uTu5ln6+tBlJXHwKTcQM75Jk91tjfGDtSAoRPZ0iduZtNO5mh
|
||||||
|
qHxcUXaqFocPscYKnY4OgfQ3UjlTHjMCAwEAAaNXMFUwDwYDVR0TAQH/BAUwAwEB
|
||||||
|
/zATBgNVHSUEDDAKBggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
|
||||||
|
FNcWV6LaBZxIcAciAhMEreA8Eb+fMA0GCSqGSIb3DQEBCwUAA4IBAQBJNkaTgCJq
|
||||||
|
RTyv0PQgt2cw96mwGmjdBbeVUrQrTdaQkSIzaekpD/5bidiD0AqZtZT2/H+BeVRW
|
||||||
|
d+fO8Rp74CipqKpEut/fl7gMbiHWGYtUxmtG+TCe2+c6syW2BEDCt+dD1mYgN7Of
|
||||||
|
itllDqh/61HhCtEiEo4HzAjSiMhmbt/uT8310pyhmtuRSgsQLyLk4F1Qlp092ObA
|
||||||
|
cLTjafd6x5TjaYMxaGXEyi7MyvHOAr+UCaotap7xQT7NdQ9ewGYtmyTBUszJ0j1i
|
||||||
|
ec/hny65gfCDk5wC6556e5YXigF/WEjV5zDykskrx4bdVoqxvJd+F1ITNrBSpGDj
|
||||||
|
7wetRUpGIcKM
|
||||||
|
-----END CERTIFICATE-----
|
||||||
BIN
mitmproxy/mitm-data/mitmproxy-ca-cert.p12
Normal file
BIN
mitmproxy/mitm-data/mitmproxy-ca-cert.p12
Normal file
Binary file not shown.
20
mitmproxy/mitm-data/mitmproxy-ca-cert.pem
Normal file
20
mitmproxy/mitm-data/mitmproxy-ca-cert.pem
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDNTCCAh2gAwIBAgIUd4CmNZbEMWsQ9UFIrXFJx87zufEwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwKDESMBAGA1UEAwwJbWl0bXByb3h5MRIwEAYDVQQKDAltaXRtcHJveHkwHhcN
|
||||||
|
MjUxMDI1MTIzMzA0WhcNMzUxMDI1MTIzMzA0WjAoMRIwEAYDVQQDDAltaXRtcHJv
|
||||||
|
eHkxEjAQBgNVBAoMCW1pdG1wcm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBALCVT6FiAvJgHkPt7lgd7ZfhD5lFmjZ/P7OmBloRZ4A/xTBVTV/a7KGJ
|
||||||
|
J7tscBRrTWihNHIQZcm8sKpCgwLrQc4SCTBlOPHQ8Imh9gLe8r1XBu+nCduScYXy
|
||||||
|
31R9Lpyh89RqW/384HgVMVKRf8qcv6qikk5+GqxZxtgIBmR0RyU+jPyBSFV0djjx
|
||||||
|
kdkuKcFF8VNayL2QCXtofBS5/+xk+iLzMBIqU8WIXGEvf2mTq8iM63bXQNVq3Iq/
|
||||||
|
zyUgOlAF++XciY6uTu5ln6+tBlJXHwKTcQM75Jk91tjfGDtSAoRPZ0iduZtNO5mh
|
||||||
|
qHxcUXaqFocPscYKnY4OgfQ3UjlTHjMCAwEAAaNXMFUwDwYDVR0TAQH/BAUwAwEB
|
||||||
|
/zATBgNVHSUEDDAKBggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
|
||||||
|
FNcWV6LaBZxIcAciAhMEreA8Eb+fMA0GCSqGSIb3DQEBCwUAA4IBAQBJNkaTgCJq
|
||||||
|
RTyv0PQgt2cw96mwGmjdBbeVUrQrTdaQkSIzaekpD/5bidiD0AqZtZT2/H+BeVRW
|
||||||
|
d+fO8Rp74CipqKpEut/fl7gMbiHWGYtUxmtG+TCe2+c6syW2BEDCt+dD1mYgN7Of
|
||||||
|
itllDqh/61HhCtEiEo4HzAjSiMhmbt/uT8310pyhmtuRSgsQLyLk4F1Qlp092ObA
|
||||||
|
cLTjafd6x5TjaYMxaGXEyi7MyvHOAr+UCaotap7xQT7NdQ9ewGYtmyTBUszJ0j1i
|
||||||
|
ec/hny65gfCDk5wC6556e5YXigF/WEjV5zDykskrx4bdVoqxvJd+F1ITNrBSpGDj
|
||||||
|
7wetRUpGIcKM
|
||||||
|
-----END CERTIFICATE-----
|
||||||
BIN
mitmproxy/mitm-data/mitmproxy-ca.p12
Normal file
BIN
mitmproxy/mitm-data/mitmproxy-ca.p12
Normal file
Binary file not shown.
47
mitmproxy/mitm-data/mitmproxy-ca.pem
Normal file
47
mitmproxy/mitm-data/mitmproxy-ca.pem
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEAsJVPoWIC8mAeQ+3uWB3tl+EPmUWaNn8/s6YGWhFngD/FMFVN
|
||||||
|
X9rsoYknu2xwFGtNaKE0chBlybywqkKDAutBzhIJMGU48dDwiaH2At7yvVcG76cJ
|
||||||
|
25JxhfLfVH0unKHz1Gpb/fzgeBUxUpF/ypy/qqKSTn4arFnG2AgGZHRHJT6M/IFI
|
||||||
|
VXR2OPGR2S4pwUXxU1rIvZAJe2h8FLn/7GT6IvMwEipTxYhcYS9/aZOryIzrdtdA
|
||||||
|
1Wrcir/PJSA6UAX75dyJjq5O7mWfr60GUlcfApNxAzvkmT3W2N8YO1IChE9nSJ25
|
||||||
|
m007maGofFxRdqoWhw+xxgqdjg6B9DdSOVMeMwIDAQABAoIBAAmvXss4r3WwvOg3
|
||||||
|
5BM0LQRgBTWAYklr6EgHqqCFBNq9ZKjWfORtgv4HIkU+2NTd38SF1vUMnjCW50+n
|
||||||
|
Tz05PwY46gUcACgPMCo0VmRo6wJkhA5f6IQA/7X7kLE3Hnfb0B5N6RbAqGUdsHNZ
|
||||||
|
ZJttxntq6EBi3T6nu+a8ZfFbiU1rDgfAMWUw0I3tvRO9jazyS1x6eQ2wYl4k0eOa
|
||||||
|
4lfo1Oj+rlZnCNPZB3aZgTi4zscDh5pnvGXLAYh/OOY4/TZ5yzbjGB8AnLrEXT99
|
||||||
|
K+VzxMrwVsRXZJoefKTvKZml/pD2FaTqbeGUKmosxxsr+9nhigh502ZbubUHVnXq
|
||||||
|
dhLQvrUCgYEA2MLry0H8K+a5r/7FMdqKIpqNaawKSHYacf5rPVrRjzyGxXFxOtQw
|
||||||
|
0AdPwQccqGL5PTHuNDrTH4411yKJLGXcrPFnpOsBCdQFUCqXf4LtObW2btwrQgX8
|
||||||
|
ON1iND/qErbqaWMNCN+XMWVkZCYXn1mR42Y93+Lo7SvcLQZQLHKIuK8CgYEA0Ix3
|
||||||
|
0Kkx3zmFDHG1G8Kyz9eLohMqO3s7TmRzuhKUCSnBZNFEvknFj5UYe6vIoZ/iVbY7
|
||||||
|
klE1HGvEKbCftlES3FBF0PCvP2Dgk53WuAOtZQ3TBeD5IvLBgmPo9t5pqpRoZx/I
|
||||||
|
QaEtp3gQBbPhad7kSVkFrURfnCy1DoKmutsQy70CgYEAt291JgSISAqwV30OGhts
|
||||||
|
TM5oH+YkqZ+wz4lT7Y8+yq2ZC9vty7AoQtP9LUg8e0+OxrfLcs3ZPtoVPCOQ3E6z
|
||||||
|
inOcl1b9APk5Kddxb8o3wV/CrFyMCwqPoPvQkJEKIJ5FD7xwGnNFOtsoMwx9by/Y
|
||||||
|
ow0yDZa0MYmtgTjXflXK2CcCgYBge5wy/RQFoibbyv9vCHSRk7cWFKfFPQ4DBpZD
|
||||||
|
z7SNSLQgYHDdWGP+OYxKKv93RvD/ln+ZAdkAfRsT7pL1VizToI+sSq3JNJixsqRU
|
||||||
|
Hd9qkSq/3YVllcnQ+UgebmeUc3SZwSp0sozcnb9L5By1TllvVbA6qRdSuZxKSke4
|
||||||
|
ywDKQQKBgCeHXftLItkzJ9xIpM7Rzb0WgKxNILo4lxk0sqmO63eBS+6WDzDQAT0S
|
||||||
|
uBH+Ed2PM8m+VEm1XZy9A7sze3xNEu84/exoD4ErW2WxUo32Xstr+NZchG3IaS5z
|
||||||
|
U045grjM1an3nqUAFaD2nc77VlJeRNA14if7RaKgzyc49ELbxt/I
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDNTCCAh2gAwIBAgIUd4CmNZbEMWsQ9UFIrXFJx87zufEwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwKDESMBAGA1UEAwwJbWl0bXByb3h5MRIwEAYDVQQKDAltaXRtcHJveHkwHhcN
|
||||||
|
MjUxMDI1MTIzMzA0WhcNMzUxMDI1MTIzMzA0WjAoMRIwEAYDVQQDDAltaXRtcHJv
|
||||||
|
eHkxEjAQBgNVBAoMCW1pdG1wcm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBALCVT6FiAvJgHkPt7lgd7ZfhD5lFmjZ/P7OmBloRZ4A/xTBVTV/a7KGJ
|
||||||
|
J7tscBRrTWihNHIQZcm8sKpCgwLrQc4SCTBlOPHQ8Imh9gLe8r1XBu+nCduScYXy
|
||||||
|
31R9Lpyh89RqW/384HgVMVKRf8qcv6qikk5+GqxZxtgIBmR0RyU+jPyBSFV0djjx
|
||||||
|
kdkuKcFF8VNayL2QCXtofBS5/+xk+iLzMBIqU8WIXGEvf2mTq8iM63bXQNVq3Iq/
|
||||||
|
zyUgOlAF++XciY6uTu5ln6+tBlJXHwKTcQM75Jk91tjfGDtSAoRPZ0iduZtNO5mh
|
||||||
|
qHxcUXaqFocPscYKnY4OgfQ3UjlTHjMCAwEAAaNXMFUwDwYDVR0TAQH/BAUwAwEB
|
||||||
|
/zATBgNVHSUEDDAKBggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
|
||||||
|
FNcWV6LaBZxIcAciAhMEreA8Eb+fMA0GCSqGSIb3DQEBCwUAA4IBAQBJNkaTgCJq
|
||||||
|
RTyv0PQgt2cw96mwGmjdBbeVUrQrTdaQkSIzaekpD/5bidiD0AqZtZT2/H+BeVRW
|
||||||
|
d+fO8Rp74CipqKpEut/fl7gMbiHWGYtUxmtG+TCe2+c6syW2BEDCt+dD1mYgN7Of
|
||||||
|
itllDqh/61HhCtEiEo4HzAjSiMhmbt/uT8310pyhmtuRSgsQLyLk4F1Qlp092ObA
|
||||||
|
cLTjafd6x5TjaYMxaGXEyi7MyvHOAr+UCaotap7xQT7NdQ9ewGYtmyTBUszJ0j1i
|
||||||
|
ec/hny65gfCDk5wC6556e5YXigF/WEjV5zDykskrx4bdVoqxvJd+F1ITNrBSpGDj
|
||||||
|
7wetRUpGIcKM
|
||||||
|
-----END CERTIFICATE-----
|
||||||
14
mitmproxy/mitm-data/mitmproxy-dhparam.pem
Normal file
14
mitmproxy/mitm-data/mitmproxy-dhparam.pem
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
-----BEGIN DH PARAMETERS-----
|
||||||
|
MIICCAKCAgEAyT6LzpwVFS3gryIo29J5icvgxCnCebcdSe/NHMkD8dKJf8suFCg3
|
||||||
|
O2+dguLakSVif/t6dhImxInJk230HmfC8q93hdcg/j8rLGJYDKu3ik6H//BAHKIv
|
||||||
|
j5O9yjU3rXCfmVJQic2Nne39sg3CreAepEts2TvYHhVv3TEAzEqCtOuTjgDv0ntJ
|
||||||
|
Gwpj+BJBRQGG9NvprX1YGJ7WOFBP/hWU7d6tgvE6Xa7T/u9QIKpYHMIkcN/l3ZFB
|
||||||
|
chZEqVlyrcngtSXCROTPcDOQ6Q8QzhaBJS+Z6rcsd7X+haiQqvoFcmaJ08Ks6LQC
|
||||||
|
ZIL2EtYJw8V8z7C0igVEBIADZBI6OTbuuhDwRw//zU1uq52Oc48CIZlGxTYG/Evq
|
||||||
|
o9EWAXUYVzWkDSTeBH1r4z/qLPE2cnhtMxbFxuvK53jGB0emy2y1Ei6IhKshJ5qX
|
||||||
|
IB/aE7SSHyQ3MDHHkCmQJCsOd4Mo26YX61NZ+n501XjqpCBQ2+DfZCBh8Va2wDyv
|
||||||
|
A2Ryg9SUz8j0AXViRNMJgJrr446yro/FuJZwnQcO3WQnXeqSBnURqKjmqkeFP+d8
|
||||||
|
6mk2tqJaY507lRNqtGlLnj7f5RNoBFJDCLBNurVgfvq9TCVWKDIFD4vZRjCrnl6I
|
||||||
|
rD693XKIHUCWOjMh1if6omGXKHH40QuME2gNa50+YPn1iYDl88uDbbMCAQI=
|
||||||
|
-----END DH PARAMETERS-----
|
||||||
9
mitmproxy/mitm-data/state.json
Normal file
9
mitmproxy/mitm-data/state.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"XboxProductList": "",
|
||||||
|
"__xbox_products__": [],
|
||||||
|
"authorization": "XBL3.0 x=14880394912820501845;eyJlbmMiOiJBMTI4Q0JDK0hTMjU2IiwiYWxnIjoiUlNBLU9BRVAiLCJjdHkiOiJKV1QiLCJ6aXAiOiJERUYiLCJ4NXQiOiJYQmRHb0FoTDJyb3FQazcwM3NHb1lnT19oM2MifQ.0dsyHwNppdvRy5AALuJwLY3dM7ItF--0F5x4mHhaoaB6k5qGo0nBppgHXxAl8Euqy5SMsyrupn6nLlv-NFKj_XS_ToZTSLR7OKEGJMgqbSBGXmmjL1Z3b2QlmirhPq2YiOQdCJLgEVr-fgODOtF4KkD_mOgLtmI5r1awbtiryYeEwVgmuePDHj28CbwdgshvTNwNNzVXJlUJCuVF1BAOHlx0kvwh2L0TmHUxS5v47trq1SbejDLABwZKmm-ACP-npOsjJNcCvqSaitCL8dLgzGWRalSBKvLEVgEyinQhn1Aootxu5h7dalDY1cUm7axU1mq9feY-fjTNJU7ILqZNiw.Vg9ZgjCQhKQuhPimfy8Auw.6YTbB772GecNu5VZT6ibSLfASHVdiB1X5DM5P_AMgyPgJ0KWWJb8pd3L9UQiPFuYHbw9ESM5Q-eiH0_XxDeTdLH8rJ3txIp5g8rSuvgz3ZxbyrIAsoHkVwRlSOTfTECJxpHiaitlFrYlzZF1zhBSKfnNT92xxu7gkN5C3NdUayKqzb1P4-d7KqNateL1bGKLUuVCterfaNkDPbuBxYdqAz0Za53kK4nHR6eOssIbe5vJihwrnNY_H6KpYqEprIW9g2IssPsBgwD6Zt16N99Fdx5X_taBBJcH8FswPBlZQ-l78TrOPlXFYt9Kyjsnu8XqBk82TfgryxHfFYVgOOCoFWuw6oMSS4LMNf5zp-WYfUCRWjMm2YCKkE894aBxbvdGUIPwATh055xrx97DIYjgutuZWG_HpXuwlqDUMTzgXPOLhhKuybinFpXd90iHvh2cst4KhVUqTDqaOM4Gm_SAqPEREDzsE5TyR30Lgo5Fi9zBLU5EhRz92Voqvz1xQmdRWU2WJ74u7Ez6g1s5C_CTFiqL-JT9SWYt4MlTz9118h-PFldkJcJm5FDlX3Yd2PoRo03fSlRUX6BDYyJVL63s_QLeFyaa0SqpX-ZFNYEgCE5X2YSAMaqmXkbGqfW90v0_DCH-hj3ul5PT0L_Q0pTaGq9Y6hW6FOGwLyZsh0xYdaGzVNrFnUjE2FJ4dMRJuSyU8imT2owugyfQLlUUrazrKjkQcKwa0HOZ1WltV8tHEjbtq0pHUw05WdeBOCF6zCuJ4i1LTAoX7h3UQFcD9hw_5huRziQObCrABVD23y4pntoeFcyFrBgXAfpkI44CsJdhr_3E8PwZjbEZsfJpoJzttD0q-h_x5LHlr_dCRoqWnUWGgatTNS9oSAA4v75X80kUg1SXrEXwyduo_0Q1wHxdR64QDHIfBVpGCKT_nCBLRdxIq8hhuhCbVC5ypPLMf-Fgg9_mbMk7A_f59suyDKd3sJ6Sm0cJ9V_jcMHAzNnz-puFpYz1rjf62_oTH6Efh187jd6FpNaFuH2Yx3SeNcbGD9fCYYxOlWW8yPqnZ1suI9rY1JY9beXoS4qMIO6N4OOiE_cyDe0jtM8jfXE1Kc98SA1nVXxnHhS1Qm64Yq4qbTCSkYYyhNJJxEorgZnHYXppo9JLR1Y8hNOHeuSgvRyxc8FR_wGDqES9dUol5g5lxuNRrDRMXIm7MKc8otEl373y_9Jy0espxQYK8KSFIIKgS5YlTDeUGFuaf6iRYcBiS1u5RRd2fKEH5DffbmATGGmprFINTKyaNWgPuGSNlBcNAUiFxjkzpt3hcvEnMR4kZW_7_adp-EIqc8P-jaSPMo7xtESvbylPlxNuyOjWDTgc2yHYRNG8-p1mgsmwTRZSbIbVC_Bl_tZBMiKgEe2K0taooPW7MHx863KRuAWzRx7_BG1duPnuPrUFZlO00bj67MeUoAQJ7Ynbhj-KEH-DhUZCyttXV0kkq5slwylyMZD0IEtc_dt7-gpSxMCVJ3p8NFQj74ctHu9Y8PVNDpqnO28cpE-TYfllT6f_s75wPoG2rGa_MAqarvn-0GX_qHOsOiFFmF_dMMSaRNvT-osLgAncViNF6-TQj5cQiJTuZ3sW0zC_8Z9MXYyQ1OijVk-9UfuMpFROcxPzMlBFE2L1RHFddl0705lgwryPaLyqhV1HUM3biIC654UBM2BMoJxXhBHFBxsre6H5TE8qocCqob-8Q5pzAnbuPiya9TLzr5j2KO5uvzqtu3i0qNmJPe0QN0VCHuvfSR-lRsgP0HNnz7q9cbrI4d8RS8Xq6UWcjKJPnQ.rxI5siBky9SiRVAJkDDF087tTDM_-rLfXcG1k3A2oDU",
|
||||||
|
"cartId": "4d2c224a-de06-46e3-96f3-fd3b401c03c2",
|
||||||
|
"fiddler.custom.cart-ms-cv": "gMXAEvO1t6dBoZCZSRPgmi.11.5",
|
||||||
|
"fiddler.custom.cart-x-authorization-muid": "D41F02B00EF54F55B781E233271D20E3",
|
||||||
|
"fiddler.custom.cart-x-ms-vector-id": "1DE426055454C535A722633B2D3B1BD85BDBB32537FB0C689A852DE57F892389"
|
||||||
|
}
|
||||||
12
mitmproxy/xboxbot/Dockerfile
Normal file
12
mitmproxy/xboxbot/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# xboxbot/Dockerfile
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装依赖:
|
||||||
|
# - python-telegram-bot 用于 Telegram 机器人 (异步 API)
|
||||||
|
RUN pip install --no-cache-dir python-telegram-bot==21.6
|
||||||
|
|
||||||
|
COPY bot.py /app/bot.py
|
||||||
|
|
||||||
|
CMD ["python", "/app/bot.py"]
|
||||||
319
mitmproxy/xboxbot/bot.py
Normal file
319
mitmproxy/xboxbot/bot.py
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import os, json, uuid, ssl, time, re
|
||||||
|
from urllib import request as urlrequest
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import (
|
||||||
|
ApplicationBuilder,
|
||||||
|
CommandHandler,
|
||||||
|
ContextTypes,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ========== 环境变量 ==========
|
||||||
|
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
|
||||||
|
|
||||||
|
# [修改] 解析允许的用户ID列表 (支持逗号分隔)
|
||||||
|
raw_ids = os.environ.get("TELEGRAM_ALLOWED_USER_ID", "")
|
||||||
|
# 去除两端空格,按逗号分割,并过滤掉空项
|
||||||
|
ALLOWED_UIDS = [x.strip() for x in raw_ids.split(',') if x.strip()]
|
||||||
|
|
||||||
|
STATE_PATH = os.environ.get("STATE_PATH", "/shared/state.json")
|
||||||
|
|
||||||
|
# 用于 /addtocart 时构造请求体
|
||||||
|
MARKET_LOCALE = os.environ.get("MITM_MARKET_LOCALE", "en-ng") # e.g. en-ng
|
||||||
|
MARKET_CODE = os.environ.get("MITM_MARKET_CODE" , "NG") # e.g. NG
|
||||||
|
|
||||||
|
UA = (
|
||||||
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_3_1 like Mac OS X) "
|
||||||
|
"AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/133.0.3065.54 "
|
||||||
|
"Version/18.0 Mobile/15E148 Safari/604.1"
|
||||||
|
)
|
||||||
|
CART_API_URL = (
|
||||||
|
"https://cart.production.store-web.dynamics.com/"
|
||||||
|
"cart/v1.0/cart/loadCart?cartType=consumer&appId=StoreWeb"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ========== 工具函数:state.json 读写 ==========
|
||||||
|
def _load_state():
|
||||||
|
try:
|
||||||
|
with open(STATE_PATH, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _save_state(st):
|
||||||
|
tmp = STATE_PATH + ".tmp"
|
||||||
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(st, f, ensure_ascii=False, indent=2, sort_keys=True)
|
||||||
|
os.replace(tmp, STATE_PATH)
|
||||||
|
|
||||||
|
def _getv(key, default=""):
|
||||||
|
st = _load_state()
|
||||||
|
return st.get(key, default)
|
||||||
|
|
||||||
|
def _setv(key, value):
|
||||||
|
st = _load_state()
|
||||||
|
st[key] = value
|
||||||
|
_save_state(st)
|
||||||
|
|
||||||
|
def _get_products(st=None):
|
||||||
|
if st is None:
|
||||||
|
st = _load_state()
|
||||||
|
arr = st.get("__xbox_products__", [])
|
||||||
|
out = []
|
||||||
|
if isinstance(arr, list):
|
||||||
|
for it in arr:
|
||||||
|
try:
|
||||||
|
p = str(it.get("ProductId","")).strip()
|
||||||
|
s = str(it.get("SkuId","")).strip()
|
||||||
|
a = str(it.get("AvailabilityId","")).strip()
|
||||||
|
if p and s and a:
|
||||||
|
out.append({"ProductId":p,"SkuId":s,"AvailabilityId":a})
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _save_products_no_notify(new_list):
|
||||||
|
"""
|
||||||
|
用在机器人手动指令时更新 products 列表和 XboxProductList,
|
||||||
|
但不发送任何 Telegram 自动推送(因为推送是 mitmproxy 插件做的)。
|
||||||
|
"""
|
||||||
|
# 去重
|
||||||
|
seen = set()
|
||||||
|
uniq = []
|
||||||
|
for it in new_list:
|
||||||
|
key = f"{it['ProductId']}||{it['SkuId']}||{it['AvailabilityId']}"
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
uniq.append(it)
|
||||||
|
|
||||||
|
st = _load_state()
|
||||||
|
st["__xbox_products__"] = uniq
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
for i, it in enumerate(uniq, 1):
|
||||||
|
parts.append(f"product{i}={it['ProductId']}|{it['SkuId']}|{it['AvailabilityId']}")
|
||||||
|
st["XboxProductList"] = ";".join(parts)
|
||||||
|
|
||||||
|
_save_state(st)
|
||||||
|
|
||||||
|
# ========== 工具函数:加购 (复用 mitmproxy 那套逻辑) ==========
|
||||||
|
def _cart_put_single(muid: str, ms_cv: str, pid: str, sid: str, aid: str, x_vec: str = ""):
|
||||||
|
"""
|
||||||
|
对单个 (ProductId, SkuId, AvailabilityId) 调用微软购物车接口。
|
||||||
|
"""
|
||||||
|
payload = {
|
||||||
|
"locale": MARKET_LOCALE,
|
||||||
|
"market": MARKET_CODE,
|
||||||
|
"catalogClientType": "storeWeb",
|
||||||
|
"friendlyName": f"cart-{MARKET_CODE}",
|
||||||
|
"riskSessionId": str(uuid.uuid4()),
|
||||||
|
"clientContext": {"client": "UniversalWebStore.Cart", "deviceType": "Pc"},
|
||||||
|
"itemsToAdd": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"productId": pid,
|
||||||
|
"skuId": sid,
|
||||||
|
"availabilityId": aid,
|
||||||
|
"campaignId": "xboxcomct",
|
||||||
|
"quantity": 1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
||||||
|
|
||||||
|
req = urlrequest.Request(CART_API_URL, data=data, method="PUT")
|
||||||
|
req.add_header("content-type", "application/json")
|
||||||
|
req.add_header("accept", "*/*")
|
||||||
|
req.add_header("x-authorization-muid", muid or "")
|
||||||
|
req.add_header("x-validation-field-1", "9pgbhbppjf2b")
|
||||||
|
req.add_header("ms-cv", ms_cv or "")
|
||||||
|
if x_vec:
|
||||||
|
req.add_header("x-ms-vector-id", x_vec)
|
||||||
|
req.add_header("accept-language", "en-US,en;q=0.9")
|
||||||
|
req.add_header("accept-encoding", "gzip, deflate, br")
|
||||||
|
req.add_header("sec-fetch-site", "cross-site")
|
||||||
|
req.add_header("sec-fetch-mode", "cors")
|
||||||
|
req.add_header("sec-fetch-dest", "empty")
|
||||||
|
req.add_header("origin", "https://www.microsoft.com")
|
||||||
|
req.add_header("referer", "https://www.microsoft.com/")
|
||||||
|
req.add_header("user-agent", UA)
|
||||||
|
|
||||||
|
ctx_ssl = ssl.create_default_context()
|
||||||
|
with urlrequest.urlopen(req, context=ctx_ssl, timeout=20) as resp:
|
||||||
|
body = resp.read()[:2048]
|
||||||
|
return resp.status, (200 <= resp.status < 300), body
|
||||||
|
|
||||||
|
# ========== 安全检查 / 回复封装 ==========
|
||||||
|
async def _auth_guard(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
uid = str(update.effective_user.id)
|
||||||
|
# [修改] 检查 ID 是否在列表中
|
||||||
|
if uid not in ALLOWED_UIDS:
|
||||||
|
await context.bot.send_message(
|
||||||
|
chat_id=update.effective_chat.id,
|
||||||
|
text=f"❌ Unauthorized (UID: {uid})", # 方便调试,显示未授权ID
|
||||||
|
disable_notification=False,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def _reply(update: Update, context: ContextTypes.DEFAULT_TYPE, text: str):
|
||||||
|
# 统一把 disable_notification=False 打开,保证有推送提醒
|
||||||
|
await context.bot.send_message(
|
||||||
|
chat_id=update.effective_chat.id,
|
||||||
|
text=text,
|
||||||
|
disable_notification=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ========== 命令实现 ==========
|
||||||
|
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not await _auth_guard(update, context):
|
||||||
|
return
|
||||||
|
await _reply(
|
||||||
|
update,
|
||||||
|
context,
|
||||||
|
"👋 你好!我是 Xbox 助手。\n可以使用 /help 查看帮助",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not await _auth_guard(update, context):
|
||||||
|
return
|
||||||
|
help_text = (
|
||||||
|
"可用指令:\n"
|
||||||
|
"/check - 查看当前关键参数(cartId等)和XboxProductList摘要\n"
|
||||||
|
"/viewproducts - 查看已捕捉到的全部三元组(ProductId, SkuId, AvailabilityId)\n"
|
||||||
|
"/clearproducts - 清空捕捉到的产品列表\n"
|
||||||
|
"/addtocart - 尝试把所有已捕捉的产品逐个加进购物车\n"
|
||||||
|
"/help - 再次查看本帮助\n"
|
||||||
|
)
|
||||||
|
await _reply(update, context, help_text)
|
||||||
|
|
||||||
|
async def cmd_check(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not await _auth_guard(update, context):
|
||||||
|
return
|
||||||
|
st = _load_state()
|
||||||
|
|
||||||
|
cart_id = st.get("cartId","")
|
||||||
|
auth = st.get("authorization","")
|
||||||
|
muid = st.get("fiddler.custom.cart-x-authorization-muid","")
|
||||||
|
ms_cv = st.get("fiddler.custom.cart-ms-cv","")
|
||||||
|
xvec = st.get("fiddler.custom.cart-x-ms-vector-id","")
|
||||||
|
plist_str = st.get("XboxProductList","")
|
||||||
|
products = _get_products(st)
|
||||||
|
countp = len(products)
|
||||||
|
|
||||||
|
text = (
|
||||||
|
"📦 /check\n"
|
||||||
|
f"cartId: {cart_id or '(none)'}\n"
|
||||||
|
f"authorization(len): {len(auth) if auth else 0}\n"
|
||||||
|
f"x-authorization-muid: {muid or '(none)'}\n"
|
||||||
|
f"ms-cv: {ms_cv or '(none)'}\n"
|
||||||
|
f"x-ms-vector-id: {xvec or '(none)'}\n"
|
||||||
|
f"XboxProductList: {plist_str or '(empty)'}\n"
|
||||||
|
f"products_count: {countp}\n"
|
||||||
|
)
|
||||||
|
await _reply(update, context, text)
|
||||||
|
|
||||||
|
async def cmd_viewproducts(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not await _auth_guard(update, context):
|
||||||
|
return
|
||||||
|
products = _get_products()
|
||||||
|
if not products:
|
||||||
|
await _reply(update, context, "📄 /viewproducts\n当前产品列表为空")
|
||||||
|
return
|
||||||
|
|
||||||
|
lines = ["📄 /viewproducts"]
|
||||||
|
for idx, it in enumerate(products, 1):
|
||||||
|
lines.append(
|
||||||
|
f"{idx}. {it['ProductId']} | {it['SkuId']} | {it['AvailabilityId']}"
|
||||||
|
)
|
||||||
|
await _reply(update, context, "\n".join(lines))
|
||||||
|
|
||||||
|
async def cmd_clearproducts(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not await _auth_guard(update, context):
|
||||||
|
return
|
||||||
|
_save_products_no_notify([]) # 清空并写回
|
||||||
|
await _reply(update, context, "🗑 已清空产品列表")
|
||||||
|
|
||||||
|
async def cmd_addtocart(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not await _auth_guard(update, context):
|
||||||
|
return
|
||||||
|
|
||||||
|
st = _load_state()
|
||||||
|
products = _get_products(st)
|
||||||
|
if not products:
|
||||||
|
await _reply(update, context, "🛒 /addtocart\n没有待加入的产品")
|
||||||
|
return
|
||||||
|
|
||||||
|
muid = st.get("fiddler.custom.cart-x-authorization-muid","")
|
||||||
|
ms_cv = st.get("fiddler.custom.cart-ms-cv","")
|
||||||
|
x_vec = st.get("fiddler.custom.cart-x-ms-vector-id","")
|
||||||
|
|
||||||
|
if (not muid) or (not ms_cv):
|
||||||
|
await _reply(update, context,
|
||||||
|
"🛒 /addtocart\n缺少 cart-x-authorization-muid 或 cart-ms-cv,无法继续")
|
||||||
|
return
|
||||||
|
|
||||||
|
successes = []
|
||||||
|
failures = []
|
||||||
|
remain = list(products)
|
||||||
|
|
||||||
|
for item in products:
|
||||||
|
pid = item["ProductId"]
|
||||||
|
sid = item["SkuId"]
|
||||||
|
aid = item["AvailabilityId"]
|
||||||
|
triple_label = f"{pid}/{sid}/{aid}"
|
||||||
|
try:
|
||||||
|
code, ok, _ = _cart_put_single(muid, ms_cv, pid, sid, aid, x_vec)
|
||||||
|
except Exception as ex:
|
||||||
|
code = 0
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
successes.append(f"{triple_label} -> {code}")
|
||||||
|
# 从 remain 里删掉成功的
|
||||||
|
remain = [
|
||||||
|
r for r in remain
|
||||||
|
if not (r["ProductId"] == pid and r["SkuId"] == sid and r["AvailabilityId"] == aid)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
failures.append(f"{triple_label} -> {code or 'ERR'}")
|
||||||
|
|
||||||
|
time.sleep(0.15)
|
||||||
|
|
||||||
|
# 更新 state.json 里的 products / XboxProductList (不推送通知,只本地改)
|
||||||
|
_save_products_no_notify(remain)
|
||||||
|
|
||||||
|
summary = (
|
||||||
|
"🛒 /addtocart 完成\n"
|
||||||
|
f"尝试: {len(products)}\n"
|
||||||
|
f"成功: {len(successes)}\n"
|
||||||
|
f"失败: {len(failures)}\n\n"
|
||||||
|
)
|
||||||
|
if successes:
|
||||||
|
summary += "✅ 成功:\n" + "\n".join(successes[:20]) + ("\n..." if len(successes) > 20 else "") + "\n"
|
||||||
|
if failures:
|
||||||
|
summary += "❌ 失败:\n" + "\n".join(failures[:20]) + ("\n..." if len(failures) > 20 else "") + "\n"
|
||||||
|
summary += f"\n剩余待加入: {len(remain)}"
|
||||||
|
|
||||||
|
await _reply(update, context, summary)
|
||||||
|
|
||||||
|
# ========== 主入口 ==========
|
||||||
|
def main():
|
||||||
|
app = ApplicationBuilder().token(BOT_TOKEN).build()
|
||||||
|
|
||||||
|
app.add_handler(CommandHandler("start", cmd_start))
|
||||||
|
app.add_handler(CommandHandler("help", cmd_help))
|
||||||
|
app.add_handler(CommandHandler("check", cmd_check))
|
||||||
|
app.add_handler(CommandHandler("viewproducts", cmd_viewproducts))
|
||||||
|
app.add_handler(CommandHandler("clearproducts",cmd_clearproducts))
|
||||||
|
app.add_handler(CommandHandler("addtocart", cmd_addtocart))
|
||||||
|
|
||||||
|
# 直接长轮询。通知静默控制在我们send_message里disable_notification=False
|
||||||
|
app.run_polling(drop_pending_updates=True)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user