接口跑通,基础功能全部实现
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
"""
|
||||
Weibo API Client
|
||||
Handles all interactions with Weibo.com, including login, sign-in, and data fetching
|
||||
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
|
||||
@@ -9,6 +16,9 @@ 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
|
||||
@@ -23,200 +33,313 @@ 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 API"""
|
||||
|
||||
"""Client for interacting with Weibo mobile API."""
|
||||
|
||||
def __init__(self):
|
||||
# Use antibot module for dynamic headers
|
||||
self.base_headers = antibot.build_headers()
|
||||
|
||||
async def verify_cookies(self, account: WeiboAccount) -> bool:
|
||||
"""Verify if Weibo cookies are still valid"""
|
||||
try:
|
||||
# Decrypt cookies using shared crypto module
|
||||
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
|
||||
|
||||
# Get proxy (with fallback to direct connection)
|
||||
proxy = await antibot.get_proxy()
|
||||
|
||||
# Use dynamic headers with random User-Agent
|
||||
headers = antibot.build_headers()
|
||||
|
||||
# Add random delay before request
|
||||
delay = antibot.get_random_delay()
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
cookies=cookies_dict,
|
||||
headers=headers,
|
||||
proxies=proxy,
|
||||
timeout=10.0
|
||||
) as client:
|
||||
response = await client.get("https://weibo.com/mygroups", follow_redirects=True)
|
||||
|
||||
if response.status_code == 200 and "我的首页" in response.text:
|
||||
logger.info(f"Cookies for account {account.weibo_user_id} are valid")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Cookies for account {account.weibo_user_id} are invalid")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying cookies: {e}")
|
||||
return False
|
||||
|
||||
async def get_super_topics(self, account: WeiboAccount) -> List[WeiboSuperTopic]:
|
||||
"""Get list of super topics for an account"""
|
||||
try:
|
||||
# Mock implementation - in real system, this would involve complex API calls
|
||||
# Simulate API call delay
|
||||
await asyncio.sleep(random.uniform(1.0, 2.0))
|
||||
|
||||
# Return mock data
|
||||
return [
|
||||
WeiboSuperTopic(id="topic_001", title="Python编程", url="...", is_signed=False),
|
||||
WeiboSuperTopic(id="topic_002", title="人工智能", url="...", is_signed=False),
|
||||
WeiboSuperTopic(id="topic_003", title="机器学习", url="...", is_signed=True)
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching super topics: {e}")
|
||||
return []
|
||||
|
||||
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
|
||||
Returns: (success, reward_info, error_message)
|
||||
"""
|
||||
try:
|
||||
# Decrypt cookies using shared crypto module
|
||||
cookies_dict = self._decrypt_cookies(account.encrypted_cookies, account.iv)
|
||||
|
||||
if not cookies_dict:
|
||||
error_msg = "Failed to decrypt cookies"
|
||||
logger.error(error_msg)
|
||||
return False, None, error_msg
|
||||
|
||||
# Get proxy (with fallback to direct connection)
|
||||
proxy = await antibot.get_proxy()
|
||||
|
||||
# Use dynamic headers with random User-Agent
|
||||
headers = antibot.build_headers()
|
||||
|
||||
# Add random delay before request (anti-bot protection)
|
||||
delay = antibot.get_random_delay()
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
# Prepare request payload
|
||||
payload = {
|
||||
"ajwvr": "6",
|
||||
"api": "http://i.huati.weibo.com/aj/super/checkin",
|
||||
"id": topic.id,
|
||||
"location": "page_100808_super_index",
|
||||
"refer_flag": "100808_-_1",
|
||||
"refer_lflag": "100808_-_1",
|
||||
"ua": headers["User-Agent"],
|
||||
"is_new": "1",
|
||||
"is_from_ad": "0",
|
||||
"ext": "mi_898_1_0_0"
|
||||
}
|
||||
|
||||
# In a real scenario, we might need to call browser automation service
|
||||
# to get signed parameters or handle JS challenges
|
||||
|
||||
# Simulate API call
|
||||
await asyncio.sleep(random.uniform(0.5, 1.5))
|
||||
|
||||
# Mock response - assume success
|
||||
response_data = {
|
||||
"code": "100000",
|
||||
"msg": "签到成功",
|
||||
"data": {
|
||||
"tip": "签到成功",
|
||||
"alert_title": "签到成功",
|
||||
"alert_subtitle": "恭喜你成为今天第12345位签到的人",
|
||||
"reward": {"exp": 2, "credit": 1}
|
||||
}
|
||||
}
|
||||
|
||||
if response_data.get("code") == "100000":
|
||||
logger.info(f"Successfully signed topic: {topic.title}")
|
||||
reward_info = response_data.get("data", {}).get("reward", {})
|
||||
return True, reward_info, None
|
||||
elif response_data.get("code") == "382004":
|
||||
logger.info(f"Topic {topic.title} already signed today")
|
||||
return True, None, "Already signed"
|
||||
else:
|
||||
error_msg = response_data.get("msg", "Unknown error")
|
||||
logger.error(f"Failed to sign topic {topic.title}: {error_msg}")
|
||||
return False, None, error_msg
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Exception signing topic {topic.title}: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return False, None, error_msg
|
||||
|
||||
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:
|
||||
# Derive encryption key from shared settings
|
||||
key = derive_key(shared_settings.COOKIE_ENCRYPTION_KEY)
|
||||
|
||||
# Decrypt using shared crypto module
|
||||
plaintext = decrypt_cookie(encrypted_cookies, iv, key)
|
||||
|
||||
# Parse cookie string into dict
|
||||
# Expected format: "key1=value1; key2=value2; ..."
|
||||
cookies_dict = {}
|
||||
for cookie_pair in plaintext.split(";"):
|
||||
cookie_pair = cookie_pair.strip()
|
||||
if "=" in cookie_pair:
|
||||
key, value = cookie_pair.split("=", 1)
|
||||
cookies_dict[key.strip()] = value.strip()
|
||||
|
||||
|
||||
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 {}
|
||||
|
||||
async def get_proxy(self) -> Optional[Dict[str, str]]:
|
||||
"""Get a proxy from the proxy pool service"""
|
||||
|
||||
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 httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(f"{settings.PROXY_POOL_URL}/get")
|
||||
if response.status_code == 200:
|
||||
proxy_info = response.json()
|
||||
return {
|
||||
"http://": f"http://{proxy_info['proxy']}",
|
||||
"https://": f"https://{proxy_info['proxy']}"
|
||||
}
|
||||
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:
|
||||
return None
|
||||
logger.warning(f"Cookies invalid for account {account.weibo_user_id}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get proxy: {e}")
|
||||
return None
|
||||
|
||||
async def get_browser_fingerprint(self) -> Dict[str, Any]:
|
||||
"""Get a browser fingerprint from the generator service"""
|
||||
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:
|
||||
# Mock implementation
|
||||
return {
|
||||
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
"screen_resolution": "1920x1080",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"plugins": ["PDF Viewer", "Chrome PDF Viewer", "Native Client"]
|
||||
}
|
||||
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"Failed to get browser fingerprint: {e}")
|
||||
return {}
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user