Files
dy/backend/rpa_engine/douyin_im/dy_util.py
T
2026-08-27 18:32:03 +08:00

213 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import hashlib
import re
import time
import json
import random
import base64
import urllib.parse
from os import path
import subprocess
original_popen = subprocess.Popen
def patched_popen(*args, **kwargs):
if kwargs.get('universal_newlines') or kwargs.get('text'):
if 'encoding' not in kwargs:
kwargs['encoding'] = 'utf-8'
return original_popen(*args, **kwargs)
subprocess.Popen = patched_popen
import execjs
import requests
basedir = path.dirname(__file__)
static_dir = path.join(basedir, 'static')
node_modules = path.join(static_dir, 'node_modules')
# 全局唯一 User-Agenta_bogus 签名、HTTP 请求头、protobuf body、webid 采集等
# 必须全部使用同一个 UA,否则抖音服务端重算 a_bogus 时与请求头 UA 不一致 -> 7911。
# 该值需与浏览器登录上下文(playwright new_context user_agent)保持一致。
DEFAULT_USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
# 动态编译 JS
dy_path = path.join(static_dir, 'dy_ab.js')
dy_js = execjs.compile(open(dy_path, 'r', encoding='utf-8').read(), cwd=node_modules)
sign_path = path.join(static_dir, 'dy_live_sign.js')
sign_js = execjs.compile(open(sign_path, 'r', encoding='utf-8').read(), cwd=node_modules)
login_path = path.join(static_dir, 'login.js')
login_js = execjs.compile(open(login_path, 'r', encoding='utf-8').read(), cwd=node_modules)
def generateSecretPhoneNum(phone):
return login_js.call('generateSecretPhoneNum', phone)
def generateSecretCode(phone, code):
return login_js.call('generateSecretCode', phone, code)
def trans_cookies(cookies_str):
cookies = {}
for i in cookies_str.split("; "):
try:
parts = i.split('=')
key = parts[0].strip()
val = '='.join(parts[1:]).strip()
# 防御性清洗:过滤掉因为误粘贴 fetch 等包含非法字符或换行的 Cookie 键
if not key or any(c in key for c in "()[]{}'\"\n \t\\"):
continue
cookies[key] = val
except:
continue
return cookies
def generate_req_sign(e, priK):
"""私信传 obj,其他的拼接"""
return dy_js.call('get_req_sign', e, priK)
def generate_a_bogus(query, data="", user_agent=None):
"""query, data 都是拼接字符串。
user_agent 必须与实际发出请求所用的 User-Agent 完全一致(见 DEFAULT_USER_AGENT),
否则抖音服务端用请求头 UA 重算 a_bogus 会对不上,导致 7911 安全校验失败。
"""
return dy_js.call('get_ab', query, data, user_agent or DEFAULT_USER_AGENT)
def generate_signature(room_id, user_unique_id):
raw_string = f"live_id=1,aid=6383,version_code=180800,webcast_sdk_version=1.0.15,room_id={room_id},sub_room_id=,sub_channel_id=,did_rule=3,user_unique_id={user_unique_id},device_platform=web,device_type=,ac=,identity=audience"
x_ms_stub = hashlib.md5(raw_string.encode("utf-8")).hexdigest()
result = sign_js.call("get_signature", x_ms_stub)
return result.get("X-Bogus")
def generate_ree_key(prik):
"""传递私钥"""
return dy_js.call('get_ree_key', prik)
def generate_bd_ticket_client_data(api, ticket, ts_sign, priK):
"""传递 query, ticket, ts_sign, priK"""
timestamp = int(time.time())
res_sign = f"ticket={ticket}&path={api}&timestamp={timestamp}"
p = {
'ts_sign': ts_sign,
'req_content': 'ticket,path,timestamp',
'req_sign': generate_req_sign(res_sign, priK),
'timestamp': timestamp,
}
p = json.dumps(p, ensure_ascii=False, separators=(',', ':'))
return base64.urlsafe_b64encode(p.encode('utf-8')).decode('utf-8')
def generate_msToken(randomlength=107):
random_str = ''
base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789='
length = len(base_str) - 1
for _ in range(randomlength):
random_str += base_str[random.randint(0, length)]
return random_str
def generate_fake_webid(random_length=19):
random_str = ''
base_str = '0123456789'
length = len(base_str) - 1
for _ in range(random_length):
random_str += base_str[random.randint(0, length)]
return random_str
def generate_webid(auth=None, url="", user_agent=""):
# 优先用已采集到的 web_id(避免每次发送都发起一次阻塞的 HTTP 请求,导致事件循环卡顿)
cached = getattr(auth, "web_id", None) if auth is not None else None
if cached:
return str(cached)
if url == "":
url = "https://www.douyin.com/discover?modal_id=7376449060384935209"
# UA 优先级:显式参数 > auth.user_agentfrom_im_session / perepare_auth 已带)> 全局默认。
# 必须与 a_bogus 签名、其余请求头使用同一个 UA,否则服务端重算失配 -> 7911。
ua = (
user_agent
or (getattr(auth, "user_agent", "") if auth is not None else "")
or DEFAULT_USER_AGENT
)
try:
from .auth import DouyinAuth
headers = {
"User-Agent": ua,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"upgrade-insecure-requests": "1"
}
if auth and auth.cookie_str:
headers['cookie'] = auth.cookie_str
try:
from rpa_engine.runtime_config import requests_proxies
proxies = requests_proxies()
except Exception:
proxies = None
response = requests.get(
url, headers=headers, verify=False, timeout=10, proxies=proxies
)
res_text = response.text
user_unique_id = re.findall(r'\\"user_unique_id\\":\\"(.*?)\\"', res_text)[0]
# 把发现的 web_id 回写到 auth 上,避免同一次同步里反复发起阻塞的 HTTP 请求
if auth is not None and user_unique_id:
try:
auth.web_id = user_unique_id
except Exception:
pass
return user_unique_id
except Exception:
# 失败时同样缓存一个伪 web_id,避免后续调用重复走 10s 超时的网络请求
fake = generate_fake_webid()
if auth is not None:
try:
if not getattr(auth, "web_id", None):
auth.web_id = fake
except Exception:
pass
return fake
def generate_millisecond():
return int(round(time.time() * 1000))
def normalize_client_cert(cert: str) -> str:
"""统一 client_cert / sdk_cert 格式为「base64 证书体」。
web_protect.client_cert 与 frontier WS 的 sdk_cert 通常已是 base64(PEM)
若误把 PEM 原文或 frontier 证书二次 base64,会导致 7911。
"""
cert = (cert or "").strip()
if not cert:
return ""
if cert.startswith("-----BEGIN"):
return base64.b64encode(cert.encode("utf-8")).decode("utf-8")
return cert
def resolve_proto_device_id(device_id: str = "", web_id: str = "", my_uid: int = 0) -> str:
"""protobuf / frontier 更倾向使用数字 device_id(通常等于 my_uid)。"""
for candidate in (device_id, web_id, str(my_uid or "")):
c = str(candidate or "").strip()
if c.isdigit():
return c
return str(device_id or web_id or "0")
def splice_url(params):
splice_url_str = ''
for key, value in params.items():
if value is None:
value = ''
splice_url_str += key + '=' + urllib.parse.quote(str(value)) + '&'
return splice_url_str[:-1]