Files
weibo_signin/backend/signin_executor/app/services/weibo_client.py

346 lines
13 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.
"""
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)