接口跑通,基础功能全部实现

This commit is contained in:
2026-03-16 16:14:08 +08:00
parent f81aec48ca
commit 2f2d5c3795
38 changed files with 3352 additions and 1754 deletions

View File

@@ -19,6 +19,8 @@ from app.services.signin_service import SignInService
from app.services.weibo_client import WeiboClient
from app.models.signin_models import SignInRequest, SignInResult, TaskStatus
logger = logging.getLogger(__name__)
# Initialize FastAPI app
app = FastAPI(
title="Weibo-HotSign Sign-in Executor",

View File

@@ -6,10 +6,9 @@ Handles Weibo super topic sign-in operations
import os
import sys
import asyncio
import httpx
import logging
import random
from datetime import datetime, timedelta
from datetime import datetime
from typing import Dict, Any, List, Optional
from uuid import UUID
from sqlalchemy import select, update
@@ -230,118 +229,116 @@ class SignInService:
logger.debug(f"Browser fingerprint: {fingerprint}")
async def _get_super_topics_list(self, account: WeiboAccount) -> List[WeiboSuperTopic]:
"""Get list of super topics for account"""
"""
Fetch the real list of followed super topics from Weibo API.
Delegates to WeiboClient.get_super_topics().
"""
try:
# Mock implementation - in real system, fetch from Weibo API
# Simulate API call delay
await asyncio.sleep(1)
# Return mock super topics
return [
WeiboSuperTopic(
id="topic_001",
title="Python编程",
url="https://weibo.com/p/100808xxx",
is_signed=False,
sign_url="https://weibo.com/p/aj/general/button",
reward_exp=2,
reward_credit=1
),
WeiboSuperTopic(
id="topic_002",
title="人工智能",
url="https://weibo.com/p/100808yyy",
is_signed=False,
sign_url="https://weibo.com/p/aj/general/button",
reward_exp=2,
reward_credit=1
),
WeiboSuperTopic(
id="topic_003",
title="机器学习",
url="https://weibo.com/p/100808zzz",
is_signed=True, # Already signed
sign_url="https://weibo.com/p/aj/general/button",
reward_exp=2,
reward_credit=1
)
]
topics = await self.weibo_client.get_super_topics(account)
logger.info(f"Fetched {len(topics)} super topics for account {account.weibo_user_id}")
return topics
except Exception as e:
logger.error(f"Error fetching super topics: {e}")
logger.error(f"Error fetching super topics for {account.weibo_user_id}: {e}")
return []
async def _execute_topic_signin(self, account: WeiboAccount, topics: List[WeiboSuperTopic], task_id: str) -> Dict[str, List[str]]:
"""Execute sign-in for each super topic"""
signed = []
already_signed = []
errors = []
for topic in topics:
"""
Execute sign-in for each super topic with retry logic and
per-topic progress updates.
"""
signed: List[str] = []
already_signed: List[str] = []
errors: List[str] = []
max_retries = 2
total = len(topics) if topics else 1
for idx, topic in enumerate(topics):
# Update progress: 50% -> 80% spread across topics
pct = 50 + int((idx / total) * 30)
await self._update_task_progress(task_id, pct)
try:
# Add small delay between requests
await asyncio.sleep(random.uniform(0.5, 1.5))
if topic.is_signed:
already_signed.append(topic.title)
# Write log for already signed
await self._write_signin_log(
account_id=str(account.id),
topic_title=topic.title,
status="failed_already_signed",
reward_info=None,
error_message="Already signed today"
error_message="Already signed today",
)
continue
# Execute signin for this topic
success, reward_info, error_msg = await self.weibo_client.sign_super_topic(
account=account,
topic=topic,
task_id=task_id
)
if success:
signed.append(topic.title)
logger.info(f"✅ Successfully signed topic: {topic.title}")
# Write success log
await self._write_signin_log(
account_id=str(account.id),
topic_title=topic.title,
status="success",
reward_info=reward_info,
error_message=None
# Retry loop
last_error: Optional[str] = None
succeeded = False
for attempt in range(1, max_retries + 1):
# Inter-request delay (longer for retries)
delay = random.uniform(1.0, 3.0) * attempt
await asyncio.sleep(delay)
success, reward_info, error_msg = await self.weibo_client.sign_super_topic(
account=account,
topic=topic,
task_id=task_id,
)
else:
errors.append(f"Failed to sign topic: {topic.title}")
# Write failure log
if success:
# "Already signed" from the API is still a success
if error_msg and "already" in error_msg.lower():
already_signed.append(topic.title)
await self._write_signin_log(
account_id=str(account.id),
topic_title=topic.title,
status="failed_already_signed",
reward_info=None,
error_message=error_msg,
)
else:
signed.append(topic.title)
logger.info(f"✅ Signed topic: {topic.title}")
await self._write_signin_log(
account_id=str(account.id),
topic_title=topic.title,
status="success",
reward_info=reward_info,
error_message=None,
)
succeeded = True
break
last_error = error_msg
logger.warning(
f"Attempt {attempt}/{max_retries} failed for "
f"{topic.title}: {error_msg}"
)
if not succeeded:
errors.append(f"{topic.title}: {last_error}")
await self._write_signin_log(
account_id=str(account.id),
topic_title=topic.title,
status="failed_network",
reward_info=None,
error_message=error_msg
error_message=last_error,
)
except Exception as e:
error_msg = f"Error signing topic {topic.title}: {str(e)}"
logger.error(error_msg)
errors.append(error_msg)
# Write error log
err = f"Error signing topic {topic.title}: {e}"
logger.error(err)
errors.append(err)
await self._write_signin_log(
account_id=str(account.id),
topic_title=topic.title,
status="failed_network",
reward_info=None,
error_message=str(e)
error_message=str(e),
)
return {
"signed": signed,
"already_signed": already_signed,
"errors": errors
"already_signed": already_signed,
"errors": errors,
}
async def _write_signin_log(

View File

@@ -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)