Add files via upload

This commit is contained in:
XXhaos
2026-04-23 17:22:20 +08:00
committed by GitHub
commit 326c416f5c
12 changed files with 1238 additions and 0 deletions

View 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)
# 抓 cartIdquery 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()]

View 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"

View 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-----

Binary file not shown.

View 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-----

Binary file not shown.

View 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-----

View 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-----

View 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"
}

View 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
View 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()