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

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

@@ -15,5 +15,9 @@ JWT_EXPIRATION_HOURS=24
# Cookie 加密密钥 (32字节)
COOKIE_ENCRYPTION_KEY=dev-cookie-encryption-key-32b
# 微信小程序配置
WX_APPID=
WX_SECRET=
# 环境
ENVIRONMENT=development

View File

@@ -3,12 +3,17 @@ Weibo Account CRUD router.
All endpoints require JWT authentication and enforce resource ownership.
"""
import logging
from datetime import datetime
from typing import Dict, List
import httpx
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from shared.models import get_db, Account, User
from shared.models import get_db, Account, SigninLog, User
from shared.crypto import encrypt_cookie, decrypt_cookie, derive_key
from shared.config import shared_settings
from shared.response import success_response, error_response
@@ -19,8 +24,20 @@ from api_service.app.schemas.account import (
AccountResponse,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/accounts", tags=["accounts"])
WEIBO_HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
),
"Referer": "https://weibo.com/",
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
}
def _encryption_key() -> bytes:
return derive_key(shared_settings.COOKIE_ENCRYPTION_KEY)
@@ -137,3 +154,312 @@ async def delete_account(
await db.delete(account)
await db.commit()
return success_response(None, "Account deleted")
# ---- helpers for verify / signin ----
def _parse_cookie_str(cookie_str: str) -> Dict[str, str]:
"""Parse 'k1=v1; k2=v2' into a dict."""
cookies: Dict[str, str] = {}
for pair in cookie_str.split(";"):
pair = pair.strip()
if "=" in pair:
k, v = pair.split("=", 1)
cookies[k.strip()] = v.strip()
return cookies
async def _verify_weibo_cookie(cookie_str: str) -> dict:
"""
Verify cookie via weibo.com PC API.
Uses /ajax/side/cards which returns ok=1 when logged in.
Returns {"valid": bool, "uid": str|None, "screen_name": str|None}.
"""
cookies = _parse_cookie_str(cookie_str)
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
# Step 1: check login via /ajax/side/cards
resp = await client.get(
"https://weibo.com/ajax/side/cards",
params={"count": "1"},
headers=WEIBO_HEADERS,
cookies=cookies,
)
data = resp.json()
if data.get("ok") != 1:
return {"valid": False, "uid": None, "screen_name": None}
# Step 2: get user info via /ajax/profile/detail
uid = None
screen_name = None
try:
resp2 = await client.get(
"https://weibo.com/ajax/profile/info",
headers=WEIBO_HEADERS,
cookies=cookies,
)
info = resp2.json()
if info.get("ok") == 1:
user = info.get("data", {}).get("user", {})
uid = str(user.get("idstr", user.get("id", "")))
screen_name = user.get("screen_name", "")
except Exception:
pass # profile info is optional, login check already passed
return {"valid": True, "uid": uid, "screen_name": screen_name}
# ---- VERIFY COOKIE ----
@router.post("/{account_id}/verify")
async def verify_account(
account_id: str,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Verify the stored cookie is still valid and update account status."""
account = await _get_owned_account(account_id, user, db)
key = _encryption_key()
try:
cookie_str = decrypt_cookie(account.encrypted_cookies, account.iv, key)
except Exception:
account.status = "invalid_cookie"
await db.commit()
await db.refresh(account)
return success_response(
{**_account_to_dict(account), "cookie_valid": False},
"Cookie decryption failed",
)
result = await _verify_weibo_cookie(cookie_str)
if result["valid"]:
account.status = "active"
account.last_checked_at = datetime.utcnow()
else:
account.status = "invalid_cookie"
await db.commit()
await db.refresh(account)
return success_response(
{**_account_to_dict(account), "cookie_valid": result["valid"],
"weibo_screen_name": result.get("screen_name")},
"Cookie verified" if result["valid"] else "Cookie is invalid or expired",
)
# ---- MANUAL SIGNIN ----
async def _get_super_topics(cookie_str: str, weibo_uid: str = "") -> List[dict]:
"""
Fetch followed super topics via weibo.com PC API.
GET /ajax/profile/topicContent?tabid=231093_-_chaohua
Returns list of {"title": str, "containerid": str}.
"""
import re
cookies = _parse_cookie_str(cookie_str)
topics: List[dict] = []
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
# First get XSRF-TOKEN by visiting weibo.com
await client.get("https://weibo.com/", headers=WEIBO_HEADERS, cookies=cookies)
xsrf = client.cookies.get("XSRF-TOKEN", "")
headers = {
**WEIBO_HEADERS,
"X-Requested-With": "XMLHttpRequest",
}
if xsrf:
headers["X-XSRF-TOKEN"] = xsrf
page = 1
max_page = 10
while page <= max_page:
params = {"tabid": "231093_-_chaohua", "page": str(page)}
resp = await client.get(
"https://weibo.com/ajax/profile/topicContent",
params=params,
headers=headers,
cookies=cookies,
)
data = resp.json()
if data.get("ok") != 1:
break
topic_list = data.get("data", {}).get("list", [])
if not topic_list:
break
for item in topic_list:
title = item.get("topic_name", "") or item.get("title", "")
# Extract containerid from oid "1022:100808xxx" or scheme
containerid = ""
oid = item.get("oid", "")
if "100808" in oid:
m = re.search(r"100808[0-9a-fA-F]+", oid)
if m:
containerid = m.group(0)
if not containerid:
scheme = item.get("scheme", "")
m = re.search(r"100808[0-9a-fA-F]+", scheme)
if m:
containerid = m.group(0)
if title and containerid:
topics.append({"title": title, "containerid": containerid})
# Check pagination
api_max = data.get("data", {}).get("max_page", 1)
if page >= api_max:
break
page += 1
return topics
async def _do_signin(cookie_str: str, topic_title: str, containerid: str) -> dict:
"""
Sign in to a single super topic via weibo.com PC API.
GET /p/aj/general/button with full browser-matching parameters.
Returns {"status": "success"|"already_signed"|"failed", "message": str}.
"""
import time as _time
cookies = _parse_cookie_str(cookie_str)
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
# Get XSRF-TOKEN
await client.get("https://weibo.com/", headers=WEIBO_HEADERS, cookies=cookies)
xsrf = client.cookies.get("XSRF-TOKEN", "")
headers = {
**WEIBO_HEADERS,
"Referer": f"https://weibo.com/p/{containerid}/super_index",
"X-Requested-With": "XMLHttpRequest",
}
if xsrf:
headers["X-XSRF-TOKEN"] = xsrf
try:
resp = await client.get(
"https://weibo.com/p/aj/general/button",
params={
"ajwvr": "6",
"api": "http://i.huati.weibo.com/aj/super/checkin",
"texta": "签到",
"textb": "已签到",
"status": "0",
"id": containerid,
"location": "page_100808_super_index",
"timezone": "GMT+0800",
"lang": "zh-cn",
"plat": "Win32",
"ua": WEIBO_HEADERS["User-Agent"],
"screen": "1920*1080",
"__rnd": str(int(_time.time() * 1000)),
},
headers=headers,
cookies=cookies,
)
data = resp.json()
code = str(data.get("code", ""))
msg = data.get("msg", "")
if code == "100000":
tip = ""
if isinstance(data.get("data"), dict):
tip = data["data"].get("alert_title", "") or data["data"].get("tipMessage", "")
return {"status": "success", "message": tip or "签到成功"}
elif code == "382004":
return {"status": "already_signed", "message": msg or "今日已签到"}
elif code == "382003":
return {"status": "failed", "message": msg or "非超话成员"}
else:
return {"status": "failed", "message": f"code={code}, msg={msg}"}
except Exception as e:
return {"status": "failed", "message": str(e)}
@router.post("/{account_id}/signin")
async def manual_signin(
account_id: str,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""
Manually trigger sign-in for all followed super topics.
Verifies cookie first, fetches topic list, signs each one, writes logs.
"""
account = await _get_owned_account(account_id, user, db)
key = _encryption_key()
# Decrypt cookie
try:
cookie_str = decrypt_cookie(account.encrypted_cookies, account.iv, key)
except Exception:
account.status = "invalid_cookie"
await db.commit()
return error_response("Cookie decryption failed", "COOKIE_ERROR", status_code=400)
# Verify cookie
verify = await _verify_weibo_cookie(cookie_str)
if not verify["valid"]:
account.status = "invalid_cookie"
await db.commit()
return error_response("Cookie is invalid or expired", "COOKIE_EXPIRED", status_code=400)
# Activate account if pending
if account.status != "active":
account.status = "active"
account.last_checked_at = datetime.utcnow()
# Get super topics
topics = await _get_super_topics(cookie_str, account.weibo_user_id)
if not topics:
await db.commit()
return success_response(
{"signed": 0, "already_signed": 0, "failed": 0, "topics": []},
"No super topics found for this account",
)
# Sign each topic
results = []
signed = already = failed = 0
for topic in topics:
import asyncio
await asyncio.sleep(1.5) # anti-bot delay
r = await _do_signin(cookie_str, topic["title"], topic["containerid"])
r["topic"] = topic["title"]
results.append(r)
# Write signin log
log = SigninLog(
account_id=account.id,
topic_title=topic["title"],
status="success" if r["status"] == "success"
else "failed_already_signed" if r["status"] == "already_signed"
else "failed_network",
reward_info={"message": r["message"]},
signed_at=datetime.utcnow(),
)
db.add(log)
if r["status"] == "success":
signed += 1
elif r["status"] == "already_signed":
already += 1
else:
failed += 1
account.last_checked_at = datetime.utcnow()
await db.commit()
return success_response(
{
"signed": signed,
"already_signed": already,
"failed": failed,
"total_topics": len(topics),
"details": results,
},
f"Signed {signed} topics, {already} already signed, {failed} failed",
)

View File

@@ -13,9 +13,11 @@ import os
import logging
from shared.models import get_db, User
from shared.config import shared_settings
from auth_service.app.models.database import create_tables
from auth_service.app.schemas.user import (
UserCreate, UserLogin, UserResponse, Token, TokenData, RefreshTokenRequest, AuthResponse,
UserCreate, UserLogin, UserResponse, Token, TokenData,
RefreshTokenRequest, AuthResponse, WxLoginRequest,
)
from auth_service.app.services.auth_service import AuthService
from auth_service.app.utils.security import (
@@ -235,3 +237,99 @@ async def get_current_user_info(current_user: UserResponse = Depends(get_current
Get current user information
"""
return current_user
@app.post("/auth/wx-login", response_model=AuthResponse)
async def wx_login(body: WxLoginRequest, db: AsyncSession = Depends(get_db)):
"""
微信小程序登录。
流程:
1. 用 code 调微信 code2Session 接口换取 openid
2. 查找是否已有该 openid 的用户
3. 没有则自动注册,有则直接登录
4. 返回 JWT token
"""
import httpx
appid = shared_settings.WX_APPID
secret = shared_settings.WX_SECRET
if not appid or not secret:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="微信小程序未配置 APPID 和 SECRET",
)
# Step 1: code 换 openid
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(
"https://api.weixin.qq.com/sns/jscode2session",
params={
"appid": appid,
"secret": secret,
"js_code": body.code,
"grant_type": "authorization_code",
},
)
wx_data = resp.json()
openid = wx_data.get("openid")
if not openid:
errcode = wx_data.get("errcode", "unknown")
errmsg = wx_data.get("errmsg", "未知错误")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"微信登录失败: {errmsg} (errcode={errcode})",
)
# Step 2: 查找已有用户
result = await db.execute(select(User).where(User.wx_openid == openid))
user = result.scalar_one_or_none()
if user:
# 已有用户 — 更新昵称头像(如果传了)
if body.nickname and body.nickname != user.wx_nickname:
user.wx_nickname = body.nickname
if body.avatar_url and body.avatar_url != user.wx_avatar:
user.wx_avatar = body.avatar_url
await db.commit()
await db.refresh(user)
else:
# Step 3: 自动注册
import uuid
nickname = body.nickname or f"wx_{openid[:8]}"
# 生成唯一 username避免冲突
username = f"wx_{openid[:12]}"
user = User(
id=str(uuid.uuid4()),
username=username,
wx_openid=openid,
wx_nickname=nickname,
wx_avatar=body.avatar_url,
is_active=True,
)
db.add(user)
await db.commit()
await db.refresh(user)
logger.info(f"微信用户自动注册: openid={openid[:16]}..., username={username}")
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="账号已被禁用",
)
# Step 4: 签发 token
access_token = create_access_token(
data={"sub": str(user.id), "username": user.username}
)
refresh_token = await create_refresh_token(str(user.id))
return AuthResponse(
access_token=access_token,
refresh_token=refresh_token,
token_type="bearer",
expires_in=3600,
user=UserResponse.from_orm(user),
)

View File

@@ -28,12 +28,17 @@ class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
is_active: Optional[bool] = None
class UserResponse(UserBase):
class UserResponse(BaseModel):
"""Schema for user response data"""
id: UUID
username: str
email: Optional[EmailStr] = None
created_at: datetime
is_active: bool
wx_openid: Optional[str] = None
wx_nickname: Optional[str] = None
wx_avatar: Optional[str] = None
class Config:
from_attributes = True # Enable ORM mode
@@ -64,3 +69,10 @@ class TokenData(BaseModel):
sub: str = Field(..., description="Subject (user ID)")
username: str = Field(..., description="Username")
exp: Optional[int] = None
class WxLoginRequest(BaseModel):
"""微信小程序登录请求"""
code: str = Field(..., description="wx.login() 获取的临时登录凭证")
nickname: Optional[str] = Field(None, max_length=100, description="微信昵称")
avatar_url: Optional[str] = Field(None, max_length=500, description="微信头像 URL")

View File

@@ -24,7 +24,11 @@ class SharedSettings(BaseSettings):
# Cookie encryption
COOKIE_ENCRYPTION_KEY: str = "change-me-in-production"
# 微信小程序
WX_APPID: str = ""
WX_SECRET: str = ""
# Environment
ENVIRONMENT: str = "development"

View File

@@ -14,8 +14,11 @@ class User(Base):
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
username = Column(String(50), unique=True, nullable=False, index=True)
email = Column(String(255), unique=True, nullable=False, index=True)
hashed_password = Column(String(255), nullable=False)
email = Column(String(255), unique=True, nullable=True, index=True)
hashed_password = Column(String(255), nullable=True)
wx_openid = Column(String(64), unique=True, nullable=True, index=True)
wx_nickname = Column(String(100), nullable=True)
wx_avatar = Column(String(500), nullable=True)
created_at = Column(DateTime, server_default=func.now())
is_active = Column(Boolean, default=True)

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)

Binary file not shown.