接口跑通,基础功能全部实现
This commit is contained in:
@@ -15,5 +15,9 @@ JWT_EXPIRATION_HOURS=24
|
||||
# Cookie 加密密钥 (32字节)
|
||||
COOKIE_ENCRYPTION_KEY=dev-cookie-encryption-key-32b
|
||||
|
||||
# 微信小程序配置
|
||||
WX_APPID=
|
||||
WX_SECRET=
|
||||
|
||||
# 环境
|
||||
ENVIRONMENT=development
|
||||
|
||||
Binary file not shown.
@@ -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",
|
||||
)
|
||||
|
||||
Binary file not shown.
@@ -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),
|
||||
)
|
||||
|
||||
Binary file not shown.
@@ -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")
|
||||
|
||||
Binary file not shown.
@@ -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"
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user