""" Weibo API Client Handles all interactions with Weibo.com, including cookie verification, super topic listing, and sign-in execution. Key Weibo API endpoints used: - Cookie验证: GET https://m.weibo.cn/api/config - 超话列表: GET https://m.weibo.cn/api/container/getIndex (containerid=100803_-_followsuper) - 超话签到: GET https://m.weibo.cn/api/container/getIndex (containerid=100808{topic_id}) POST https://huati.weibo.cn/aj/super/checkin (actual sign-in) """ import os import sys import httpx import asyncio import logging import random import json import re import time from typing import Dict, Any, Optional, List, Tuple # Add parent directory to path for imports sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../..")) from shared.crypto import decrypt_cookie, derive_key from shared.config import shared_settings from app.config import settings from app.models.signin_models import WeiboAccount, WeiboSuperTopic from app.services.antibot import antibot logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Constants – Weibo mobile API base URLs # --------------------------------------------------------------------------- WEIBO_PC_API = "https://weibo.com" WEIBO_HUATI_CHECKIN = "http://i.huati.weibo.com/aj/super/checkin" class WeiboClient: """Client for interacting with Weibo mobile API.""" def __init__(self): self.timeout = httpx.Timeout(15.0, connect=10.0) # ------------------------------------------------------------------ # Cookie helpers # ------------------------------------------------------------------ def _decrypt_cookies(self, encrypted_cookies: str, iv: str) -> Dict[str, str]: """ Decrypt cookies using AES-256-GCM from shared crypto module. Returns dict of cookie key-value pairs. """ try: key = derive_key(shared_settings.COOKIE_ENCRYPTION_KEY) plaintext = decrypt_cookie(encrypted_cookies, iv, key) cookies_dict: Dict[str, str] = {} for pair in plaintext.split(";"): pair = pair.strip() if "=" in pair: k, v = pair.split("=", 1) cookies_dict[k.strip()] = v.strip() return cookies_dict except Exception as e: logger.error(f"Failed to decrypt cookies: {e}") return {} def _build_client( self, cookies: Dict[str, str], headers: Optional[Dict[str, str]] = None, proxy: Optional[Dict[str, str]] = None, ) -> httpx.AsyncClient: """Create a configured httpx.AsyncClient for weibo.com PC API.""" hdrs = headers or antibot.build_headers() hdrs["Referer"] = "https://weibo.com/" hdrs["Accept"] = "*/*" hdrs["Accept-Language"] = "zh-CN,zh;q=0.9,en;q=0.8" return httpx.AsyncClient( cookies=cookies, headers=hdrs, proxies=proxy, timeout=self.timeout, follow_redirects=True, ) # ------------------------------------------------------------------ # 1. Cookie verification # ------------------------------------------------------------------ async def verify_cookies(self, account: WeiboAccount) -> bool: """ Verify if Weibo cookies are still valid using the PC API. GET https://weibo.com/ajax/side/cards?count=1 Returns ok=1 when logged in. """ cookies_dict = self._decrypt_cookies(account.encrypted_cookies, account.iv) if not cookies_dict: logger.error(f"Failed to decrypt cookies for account {account.weibo_user_id}") return False proxy = await antibot.get_proxy() headers = antibot.build_headers() delay = antibot.get_random_delay() await asyncio.sleep(delay) try: async with self._build_client(cookies_dict, headers, proxy) as client: resp = await client.get( f"{WEIBO_PC_API}/ajax/side/cards", params={"count": "1"}, ) if resp.status_code != 200: logger.warning( f"Side cards API returned {resp.status_code} for {account.weibo_user_id}" ) return False data = resp.json() if data.get("ok") == 1: logger.info(f"Cookies valid for account {account.weibo_user_id}") return True else: logger.warning(f"Cookies invalid for account {account.weibo_user_id}") return False except Exception as e: logger.error(f"Error verifying cookies for {account.weibo_user_id}: {e}") return False # ------------------------------------------------------------------ # 2. Fetch super topic list # ------------------------------------------------------------------ async def get_super_topics(self, account: WeiboAccount) -> List[WeiboSuperTopic]: """ Fetch the list of super topics the account has followed. Uses the PC API: GET https://weibo.com/ajax/profile/topicContent?tabid=231093_-_chaohua&page={n} Response contains data.list[] with topic objects including: - topic_name: super topic name - oid: "1022:100808xxx" (containerid) - scheme: "sinaweibo://pageinfo?containerid=100808xxx" """ cookies_dict = self._decrypt_cookies(account.encrypted_cookies, account.iv) if not cookies_dict: return [] proxy = await antibot.get_proxy() headers = antibot.build_headers() all_topics: List[WeiboSuperTopic] = [] page = 1 max_pages = 10 try: async with self._build_client(cookies_dict, headers, proxy) as client: # Get XSRF-TOKEN first await client.get(f"{WEIBO_PC_API}/", params={}) xsrf = client.cookies.get("XSRF-TOKEN", "") if xsrf: client.headers["X-XSRF-TOKEN"] = xsrf client.headers["X-Requested-With"] = "XMLHttpRequest" while page <= max_pages: delay = antibot.get_random_delay() await asyncio.sleep(delay) resp = await client.get( f"{WEIBO_PC_API}/ajax/profile/topicContent", params={ "tabid": "231093_-_chaohua", "page": str(page), }, ) if resp.status_code != 200: logger.warning(f"Topic list API returned {resp.status_code}") break body = resp.json() if body.get("ok") != 1: break topic_list = body.get("data", {}).get("list", []) if not topic_list: break for item in topic_list: topic = self._parse_pc_topic(item) if topic: all_topics.append(topic) logger.info( f"Page {page}: found {len(topic_list)} topics " f"(total so far: {len(all_topics)})" ) api_max = body.get("data", {}).get("max_page", 1) if page >= api_max: break page += 1 except Exception as e: logger.error(f"Error fetching super topics: {e}") logger.info( f"Fetched {len(all_topics)} super topics for account {account.weibo_user_id}" ) return all_topics def _parse_pc_topic(self, item: Dict[str, Any]) -> Optional[WeiboSuperTopic]: """Parse a topic item from /ajax/profile/topicContent response.""" title = item.get("topic_name", "") or item.get("title", "") if not title: return None # Extract containerid from oid "1022:100808xxx" or scheme containerid = "" oid = item.get("oid", "") if "100808" in oid: match = re.search(r"100808[0-9a-fA-F]+", oid) if match: containerid = match.group(0) if not containerid: scheme = item.get("scheme", "") match = re.search(r"100808[0-9a-fA-F]+", scheme) if match: containerid = match.group(0) if not containerid: return None return WeiboSuperTopic( id=containerid, title=title, url=f"https://weibo.com/p/{containerid}/super_index", is_signed=False, sign_url=f"{WEIBO_PC_API}/p/aj/general/button", ) # ------------------------------------------------------------------ # 3. Execute sign-in for a single super topic # ------------------------------------------------------------------ async def sign_super_topic( self, account: WeiboAccount, topic: WeiboSuperTopic, task_id: str, ) -> Tuple[bool, Optional[Dict[str, Any]], Optional[str]]: """ Execute sign-in for a single super topic via weibo.com PC API. GET https://weibo.com/p/aj/general/button ?api=http://i.huati.weibo.com/aj/super/checkin&id={topic_id} Returns: (success, reward_info, error_message) """ cookies_dict = self._decrypt_cookies(account.encrypted_cookies, account.iv) if not cookies_dict: return False, None, "Failed to decrypt cookies" proxy = await antibot.get_proxy() headers = antibot.build_headers() # Anti-bot delay delay = antibot.get_random_delay() await asyncio.sleep(delay) try: headers["Referer"] = f"https://weibo.com/p/{topic.id}/super_index" headers["X-Requested-With"] = "XMLHttpRequest" async with httpx.AsyncClient( cookies=cookies_dict, headers=headers, proxies=proxy, timeout=self.timeout, follow_redirects=True, ) as client: # Get XSRF-TOKEN await client.get(f"{WEIBO_PC_API}/") xsrf = client.cookies.get("XSRF-TOKEN", "") if xsrf: client.headers["X-XSRF-TOKEN"] = xsrf resp = await client.get( f"{WEIBO_PC_API}/p/aj/general/button", params={ "ajwvr": "6", "api": WEIBO_HUATI_CHECKIN, "texta": "签到", "textb": "已签到", "status": "0", "id": topic.id, "location": "page_100808_super_index", "timezone": "GMT+0800", "lang": "zh-cn", "plat": "Win32", "ua": headers.get("User-Agent", ""), "screen": "1920*1080", "__rnd": str(int(time.time() * 1000)), }, ) if resp.status_code != 200: return False, None, f"HTTP {resp.status_code}" body = resp.json() code = str(body.get("code", "")) msg = body.get("msg", "") data = body.get("data", {}) if code == "100000": tip = "" if isinstance(data, dict): tip = data.get("alert_title", "") or data.get("tipMessage", "签到成功") logger.info(f"Checkin success for {topic.title}: {tip}") reward_info = {} if isinstance(data, dict): reward_info = { "tip": data.get("tipMessage", ""), "alert_title": data.get("alert_title", ""), "alert_subtitle": data.get("alert_subtitle", ""), } return True, reward_info, None elif code == "382004": logger.info(f"Topic {topic.title} already signed today") return False, None, "Already signed today" elif code == "382003": logger.warning(f"Not a member of topic {topic.title}") return False, None, "Not a member of this super topic" else: logger.warning(f"Checkin unexpected code={code} msg={msg} for {topic.title}") return False, None, f"Unexpected: code={code}, msg={msg}" except httpx.TimeoutException: return False, None, "Request timeout" except Exception as e: logger.error(f"Checkin error for {topic.title}: {e}") return False, None, str(e)