"""私信对方用户资料抓取(昵称 / 头像 / UID)。""" from __future__ import annotations import asyncio import logging from typing import Any, Optional import requests from rpa_engine.device_profiles import resolve_user_agent from .auth import DouyinAuth from .conv_util import resolve_peer_uid from .dy_util import ( DEFAULT_USER_AGENT, generate_a_bogus, generate_msToken, generate_webid, splice_url, ) from .protocol import _pick_avatar_url from .session import DouyinImSession logger = logging.getLogger("douyin_im.peer_profile") _profile_cache: dict[str, dict[str, str]] = {} def _cache_key(account_id: int, peer_uid: str) -> str: return f"{account_id}:{peer_uid}" def _pick_str(data: dict, *keys: str) -> str: for key in keys: value = data.get(key) if value is not None and str(value).strip(): return str(value).strip() return "" def _extract_profile_from_payload(data: Any) -> dict[str, str]: if not isinstance(data, dict): return {} nodes = [data, data.get("user"), data.get("user_info"), data.get("data")] for node in nodes: if not isinstance(node, dict): continue uid = _pick_str(node, "uid", "user_id", "user_uid", "id") nickname = _pick_str( node, "nickname", "nick_name", "unique_id", "display_name", "name", ) avatar = _pick_avatar_url(node) if uid or nickname or avatar: return {"uid": uid, "nickname": nickname, "avatar_url": avatar} return {} def _requests_proxies() -> dict | None: try: from rpa_engine.runtime_config import requests_proxies return requests_proxies() except Exception: return None def _build_auth(session: DouyinImSession) -> tuple[DouyinAuth, str]: auth = DouyinAuth() auth.perepare_auth(session.cookie_header(), session.web_protect_str, session.keys_str) ua = resolve_user_agent(session.user_agent or DEFAULT_USER_AGENT) return auth, ua def is_generic_peer_name(name: str, peer_uid: str = "") -> bool: value = (name or "").strip() if not value: return True if peer_uid and value == peer_uid: return True if value.isdigit(): return True if value.startswith("用户") and value[2:].isdigit(): return True if value.startswith("会话") and len(value) <= 16: return True return False def fetch_peer_profile_sync( session: DouyinImSession, peer_uid: int | str, account_id: int = 0, ) -> dict[str, str]: uid = str(peer_uid or "").strip() if not uid.isdigit(): return {} cache_key = _cache_key(account_id, uid) cached = _profile_cache.get(cache_key) if cached: return dict(cached) result = {"uid": uid, "nickname": "", "avatar_url": ""} try: auth, ua = _build_auth(session) except Exception as exc: logger.warning(f"build auth for peer profile failed: {exc}") return result headers = { "User-Agent": ua, "Referer": "https://www.douyin.com/", "Accept": "application/json, text/plain, */*", } base_params = { "device_platform": "webapp", "aid": "6383", "channel": "channel_pc_web", "publish_video_strategy_type": "2", "user_id": uid, "sec_user_id": "", "verifyFp": auth.cookie.get("s_v_web_id", "") if auth.cookie else "", "fp": auth.cookie.get("s_v_web_id", "") if auth.cookie else "", "webid": generate_webid(auth, "https://www.douyin.com/"), "msToken": auth.msToken or generate_msToken(), } endpoints = [ "https://www.douyin.com/aweme/v1/web/user/profile/other/", "https://www.douyin.com/aweme/v1/web/im/user/info/", ] proxies = _requests_proxies() for url in endpoints: try: params = dict(base_params) query = splice_url(params) params["a_bogus"] = generate_a_bogus(query, user_agent=ua) resp = requests.get( url, params=params, headers=headers, cookies=auth.cookie, verify=False, timeout=12, proxies=proxies, ) data = resp.json() extracted = _extract_profile_from_payload(data) if extracted.get("uid") and not result["uid"]: result["uid"] = extracted["uid"] if extracted.get("nickname") and not result["nickname"]: result["nickname"] = extracted["nickname"] if extracted.get("avatar_url") and not result["avatar_url"]: result["avatar_url"] = extracted["avatar_url"] if result["nickname"] and result["avatar_url"]: break except Exception as exc: logger.debug(f"peer profile fetch failed for {url}: {exc}") if result["nickname"] or result["avatar_url"]: _profile_cache[cache_key] = dict(result) return result async def fetch_peer_profile( session: DouyinImSession, peer_uid: int | str, account_id: int = 0, ) -> dict[str, str]: return await asyncio.to_thread(fetch_peer_profile_sync, session, peer_uid, account_id) def enrich_conversation_item(conv: dict, my_uid: int = 0) -> dict: """补全会话项中的 peer_uid / sender_id。""" item = dict(conv or {}) conv_id = str(item.get("conversation_id") or "").strip() peer_uid = str(item.get("peer_uid") or item.get("sender_id") or "").strip() if (not peer_uid or not peer_uid.isdigit()) and conv_id and my_uid: resolved = resolve_peer_uid(conv_id, int(my_uid)) if resolved: peer_uid = str(resolved) if peer_uid: item["peer_uid"] = peer_uid item["sender_id"] = peer_uid elif conv_id: item["sender_id"] = conv_id return item