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

346 lines
13 KiB
Python
Raw Normal View History

2026-03-09 14:05:00 +08:00
"""
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)
2026-03-09 14:05:00 +08:00
"""
2026-03-09 16:10:29 +08:00
import os
import sys
2026-03-09 14:05:00 +08:00
import httpx
import asyncio
import logging
import random
import json
import re
import time
2026-03-09 16:10:29 +08:00
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
2026-03-09 14:05:00 +08:00
from app.config import settings
from app.models.signin_models import WeiboAccount, WeiboSuperTopic
2026-03-09 16:10:29 +08:00
from app.services.antibot import antibot
2026-03-09 14:05:00 +08:00
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"
2026-03-09 14:05:00 +08:00
class WeiboClient:
"""Client for interacting with Weibo mobile API."""
2026-03-09 14:05:00 +08:00
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
# ------------------------------------------------------------------
2026-03-09 14:05:00 +08:00
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)
2026-03-09 14:05:00 +08:00
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}")
2026-03-09 14:05:00 +08:00
return True
else:
logger.warning(f"Cookies invalid for account {account.weibo_user_id}")
2026-03-09 14:05:00 +08:00
return False
except Exception as e:
logger.error(f"Error verifying cookies for {account.weibo_user_id}: {e}")
2026-03-09 14:05:00 +08:00
return False
# ------------------------------------------------------------------
# 2. Fetch super topic list
# ------------------------------------------------------------------
2026-03-09 14:05:00 +08:00
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
2026-03-09 14:05:00 +08:00
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
2026-03-09 14:05:00 +08:00
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
# ------------------------------------------------------------------
2026-03-09 16:10:29 +08:00
async def sign_super_topic(
self,
account: WeiboAccount,
topic: WeiboSuperTopic,
task_id: str,
2026-03-09 16:10:29 +08:00
) -> Tuple[bool, Optional[Dict[str, Any]], Optional[str]]:
2026-03-09 14:05:00 +08:00
"""
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}
2026-03-09 16:10:29 +08:00
Returns: (success, reward_info, error_message)
2026-03-09 14:05:00 +08:00
"""
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)
2026-03-09 14:05:00 +08:00
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"
2026-03-09 14:05:00 +08:00
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"
2026-03-09 14:05:00 +08:00
except Exception as e:
logger.error(f"Checkin error for {topic.title}: {e}")
return False, None, str(e)