123
This commit is contained in:
34
backend/signin_executor/Dockerfile
Normal file
34
backend/signin_executor/Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
||||
# Weibo-HotSign Sign-in Executor Service Dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
default-libmysqlclient-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements first for better caching
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY app/ ./app/
|
||||
|
||||
# Create non-root user for security
|
||||
RUN groupadd -r appuser && useradd -r -g appuser appuser
|
||||
USER appuser
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
# Start application
|
||||
CMD ["python", "-m", "app.main"]
|
||||
56
backend/signin_executor/app/config.py
Normal file
56
backend/signin_executor/app/config.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Configuration for Sign-in Executor Service
|
||||
"""
|
||||
|
||||
import os
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Sign-in Executor settings"""
|
||||
|
||||
# Server settings
|
||||
HOST: str = os.getenv("HOST", "0.0.0.0")
|
||||
PORT: int = int(os.getenv("PORT", 8000))
|
||||
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
|
||||
|
||||
# Database settings
|
||||
DATABASE_URL: str = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"mysql+aiomysql://weibo:123456789@118.195.133.163/weibo"
|
||||
)
|
||||
REDIS_URL: str = os.getenv("REDIS_URL", "redis://redis:6379")
|
||||
|
||||
# External service URLs
|
||||
PROXY_POOL_URL: str = os.getenv("PROXY_POOL_URL", "http://proxy-pool:8080")
|
||||
BROWSER_AUTOMATION_URL: str = os.getenv("BROWSER_AUTOMATION_URL", "http://browser-automation:3001")
|
||||
TASK_SCHEDULER_URL: str = os.getenv("TASK_SCHEDULER_URL", "http://task-scheduler:8000")
|
||||
|
||||
# Weibo API settings
|
||||
WEIBO_LOGIN_URL: str = "https://weibo.com/login.php"
|
||||
WEIBO_SUPER_TOPIC_URL: str = "https://weibo.com/p/aj/general/button"
|
||||
|
||||
# Anti-bot protection settings
|
||||
RANDOM_DELAY_MIN: float = float(os.getenv("RANDOM_DELAY_MIN", "1.0"))
|
||||
RANDOM_DELAY_MAX: float = float(os.getenv("RANDOM_DELAY_MAX", "3.0"))
|
||||
USER_AGENT_ROTATION: bool = os.getenv("USER_AGENT_ROTATION", "True").lower() == "true"
|
||||
|
||||
# Cookie and session settings
|
||||
COOKIE_ENCRYPTION_KEY: str = os.getenv("COOKIE_ENCRYPTION_KEY", "your-cookie-encryption-key")
|
||||
SESSION_TIMEOUT_MINUTES: int = int(os.getenv("SESSION_TIMEOUT_MINUTES", "30"))
|
||||
|
||||
# Browser automation settings
|
||||
BROWSER_HEADLESS: bool = os.getenv("BROWSER_HEADLESS", "True").lower() == "true"
|
||||
BROWSER_TIMEOUT_SECONDS: int = int(os.getenv("BROWSER_TIMEOUT_SECONDS", "30"))
|
||||
|
||||
# Task execution settings
|
||||
MAX_CONCURRENT_SIGNIN: int = int(os.getenv("MAX_CONCURRENT_SIGNIN", "5"))
|
||||
TASK_TIMEOUT_SECONDS: int = int(os.getenv("TASK_TIMEOUT_SECONDS", "300"))
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
|
||||
|
||||
class Config:
|
||||
case_sensitive = True
|
||||
env_file = ".env"
|
||||
|
||||
settings = Settings()
|
||||
226
backend/signin_executor/app/main.py
Normal file
226
backend/signin_executor/app/main.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
Weibo-HotSign Sign-in Executor Service
|
||||
Core service that executes sign-in tasks and handles Weibo interactions
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, BackgroundTasks, HTTPException, status, Depends, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
import uvicorn
|
||||
import asyncio
|
||||
import httpx
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
import os
|
||||
|
||||
from app.config import settings
|
||||
from app.services.signin_service import SignInService
|
||||
from app.services.weibo_client import WeiboClient
|
||||
from app.models.signin_models import SignInRequest, SignInResult, TaskStatus
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title="Weibo-HotSign Sign-in Executor",
|
||||
description="Core service for executing Weibo super topic sign-in tasks",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
# CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # In production, specify actual origins
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Initialize services
|
||||
signin_service = SignInService()
|
||||
weibo_client = WeiboClient()
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Initialize executor service on startup"""
|
||||
print("🚀 Weibo-HotSign Sign-in Executor starting up...")
|
||||
print(f"📡 Service Documentation: http://{settings.HOST}:{settings.PORT}/docs")
|
||||
print("🔧 Ready to process sign-in tasks...")
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
"""Cleanup on shutdown"""
|
||||
print("👋 Weibo-HotSign Sign-in Executor shutting down...")
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"service": "Weibo-HotSign Sign-in Executor",
|
||||
"status": "running",
|
||||
"version": "1.0.0",
|
||||
"description": "Core sign-in execution service for Weibo super topics",
|
||||
"capabilities": [
|
||||
"Weibo login and verification",
|
||||
"Super topic sign-in automation",
|
||||
"Anti-bot protection handling",
|
||||
"Proxy integration",
|
||||
"Browser fingerprint simulation"
|
||||
]
|
||||
}
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "signin-executor",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"dependencies": {
|
||||
"database": "connected",
|
||||
"redis": "connected",
|
||||
"proxy_pool": f"{settings.PROXY_POOL_URL}",
|
||||
"browser_automation": f"{settings.BROWSER_AUTOMATION_URL}"
|
||||
}
|
||||
}
|
||||
|
||||
@app.post("/api/v1/signin/execute", response_model=SignInResult)
|
||||
async def execute_signin_task(
|
||||
signin_request: SignInRequest,
|
||||
background_tasks: BackgroundTasks
|
||||
):
|
||||
"""
|
||||
Execute sign-in task for specified account
|
||||
This endpoint is called by the task scheduler
|
||||
"""
|
||||
try:
|
||||
logger.info(f"🎯 Received sign-in request for account: {signin_request.account_id}")
|
||||
|
||||
# Execute sign-in in background to avoid timeout
|
||||
background_tasks.add_task(
|
||||
signin_service.execute_signin_task,
|
||||
signin_request.account_id,
|
||||
signin_request.task_id
|
||||
)
|
||||
|
||||
# Return immediate response
|
||||
return SignInResult(
|
||||
task_id=signin_request.task_id,
|
||||
account_id=signin_request.account_id,
|
||||
status="accepted",
|
||||
message="Sign-in task accepted and queued for execution",
|
||||
started_at=datetime.now(),
|
||||
estimated_completion=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Failed to accept sign-in task: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to accept sign-in task: {str(e)}"
|
||||
)
|
||||
|
||||
@app.get("/api/v1/signin/status/{task_id}", response_model=TaskStatus)
|
||||
async def get_task_status(task_id: str):
|
||||
"""Get status of a sign-in task"""
|
||||
try:
|
||||
status_info = await signin_service.get_task_status(task_id)
|
||||
if not status_info:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Task {task_id} not found"
|
||||
)
|
||||
return status_info
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error getting task status: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Internal server error"
|
||||
)
|
||||
|
||||
@app.post("/api/v1/signin/test")
|
||||
async def test_signin_capability():
|
||||
"""Test sign-in service capabilities (for debugging)"""
|
||||
try:
|
||||
# Test basic service connectivity
|
||||
tests = {
|
||||
"weibo_connectivity": await _test_weibo_connectivity(),
|
||||
"proxy_pool_access": await _test_proxy_pool(),
|
||||
"browser_automation": await _test_browser_automation(),
|
||||
"database_connection": await _test_database_connection()
|
||||
}
|
||||
|
||||
return {
|
||||
"test_timestamp": datetime.now().isoformat(),
|
||||
"tests": tests,
|
||||
"overall_status": "operational" if all(tests.values()) else "degraded"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Capability test failed: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Capability test failed: {str(e)}"
|
||||
)
|
||||
|
||||
async def _test_weibo_connectivity() -> bool:
|
||||
"""Test connectivity to Weibo"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get("https://weibo.com", follow_redirects=True)
|
||||
return response.status_code == 200
|
||||
except:
|
||||
return False
|
||||
|
||||
async def _test_proxy_pool() -> bool:
|
||||
"""Test proxy pool service availability"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(f"{settings.PROXY_POOL_URL}/health", timeout=5.0)
|
||||
return response.status_code == 200
|
||||
except:
|
||||
return False
|
||||
|
||||
async def _test_browser_automation() -> bool:
|
||||
"""Test browser automation service availability"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(f"{settings.BROWSER_AUTOMATION_URL}/health", timeout=5.0)
|
||||
return response.status_code == 200
|
||||
except:
|
||||
return False
|
||||
|
||||
async def _test_database_connection() -> bool:
|
||||
"""Test database connectivity"""
|
||||
try:
|
||||
# Simple database ping test
|
||||
return True # Simplified for demo
|
||||
except:
|
||||
return False
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
"""Global HTTP exception handler"""
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
"success": False,
|
||||
"data": None,
|
||||
"message": exc.detail,
|
||||
"error": {
|
||||
"code": f"HTTP_{exc.status_code}",
|
||||
"details": []
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
host = os.getenv("HOST", settings.HOST)
|
||||
port = int(os.getenv("PORT", settings.PORT))
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level="info" if not settings.DEBUG else "debug"
|
||||
)
|
||||
89
backend/signin_executor/app/models/signin_models.py
Normal file
89
backend/signin_executor/app/models/signin_models.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Data models for Sign-in Executor Service
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
class SignInRequest(BaseModel):
|
||||
"""Request model for sign-in task execution"""
|
||||
task_id: str = Field(..., description="Unique task identifier")
|
||||
account_id: str = Field(..., description="Weibo account identifier")
|
||||
timestamp: Optional[datetime] = Field(default_factory=datetime.now, description="Request timestamp")
|
||||
requested_by: Optional[str] = Field(default="task_scheduler", description="Request source")
|
||||
|
||||
class SignInResult(BaseModel):
|
||||
"""Result model for sign-in task execution"""
|
||||
task_id: str = Field(..., description="Task identifier")
|
||||
account_id: str = Field(..., description="Account identifier")
|
||||
status: str = Field(..., description="Task status: accepted, running, success, failed")
|
||||
message: str = Field(..., description="Human readable result message")
|
||||
started_at: datetime = Field(..., description="Task start timestamp")
|
||||
completed_at: Optional[datetime] = Field(None, description="Task completion timestamp")
|
||||
estimated_completion: Optional[datetime] = Field(None, description="Estimated completion time")
|
||||
reward_info: Optional[Dict[str, Any]] = Field(None, description="Reward details like exp, credits")
|
||||
error_message: Optional[str] = Field(None, description="Error details if failed")
|
||||
signed_topics: Optional[List[str]] = Field(None, description="List of successfully signed topics")
|
||||
total_topics: Optional[int] = Field(None, description="Total number of topics attempted")
|
||||
|
||||
class TaskStatus(BaseModel):
|
||||
"""Status model for tracking sign-in task progress"""
|
||||
task_id: str = Field(..., description="Task identifier")
|
||||
account_id: str = Field(..., description="Account identifier")
|
||||
status: str = Field(..., description="Current status: pending, running, success, failed")
|
||||
progress_percentage: int = Field(default=0, ge=0, le=100, description="Progress percentage")
|
||||
current_step: Optional[str] = Field(None, description="Current execution step")
|
||||
steps_completed: List[str] = Field(default_factory=list, description="Completed steps")
|
||||
steps_remaining: List[str] = Field(default_factory=list, description="Remaining steps")
|
||||
started_at: datetime = Field(..., description="Start timestamp")
|
||||
updated_at: datetime = Field(default_factory=datetime.now, description="Last update timestamp")
|
||||
estimated_completion: Optional[datetime] = Field(None, description="Estimated completion")
|
||||
|
||||
class WeiboAccount(BaseModel):
|
||||
"""Weibo account information for sign-in"""
|
||||
id: UUID = Field(..., description="Account UUID")
|
||||
user_id: UUID = Field(..., description="Owner user UUID")
|
||||
weibo_user_id: str = Field(..., description="Weibo user ID")
|
||||
remark: Optional[str] = Field(None, description="User remark")
|
||||
encrypted_cookies: str = Field(..., description="Encrypted Weibo cookies")
|
||||
iv: str = Field(..., description="Encryption initialization vector")
|
||||
status: str = Field(default="active", description="Account status: active, invalid_cookie, banned")
|
||||
last_checked_at: Optional[datetime] = Field(None, description="Last validation timestamp")
|
||||
|
||||
class SignInLog(BaseModel):
|
||||
"""Sign-in operation log entry"""
|
||||
id: Optional[int] = Field(None, description="Log entry ID")
|
||||
account_id: UUID = Field(..., description="Account UUID")
|
||||
topic_title: Optional[str] = Field(None, description="Signed topic title")
|
||||
status: str = Field(..., description="Sign-in status")
|
||||
reward_info: Optional[Dict[str, Any]] = Field(None, description="Reward information")
|
||||
error_message: Optional[str] = Field(None, description="Error details")
|
||||
signed_at: datetime = Field(default_factory=datetime.now, description="Sign-in timestamp")
|
||||
execution_time_ms: Optional[int] = Field(None, description="Execution time in milliseconds")
|
||||
|
||||
class WeiboSuperTopic(BaseModel):
|
||||
"""Weibo super topic information"""
|
||||
id: str = Field(..., description="Topic ID")
|
||||
title: str = Field(..., description="Topic title")
|
||||
url: str = Field(..., description="Topic URL")
|
||||
is_signed: bool = Field(default=False, description="Whether already signed")
|
||||
sign_url: Optional[str] = Field(None, description="Sign-in API URL")
|
||||
reward_exp: Optional[int] = Field(None, description="Experience points reward")
|
||||
reward_credit: Optional[int] = Field(None, description="Credit points reward")
|
||||
|
||||
class AntiBotConfig(BaseModel):
|
||||
"""Anti-bot protection configuration"""
|
||||
random_delay_min: float = Field(default=1.0, description="Minimum random delay seconds")
|
||||
random_delay_max: float = Field(default=3.0, description="Maximum random delay seconds")
|
||||
user_agent_rotation: bool = Field(default=True, description="Enable user agent rotation")
|
||||
proxy_enabled: bool = Field(default=True, description="Enable proxy usage")
|
||||
fingerprint_simulation: bool = Field(default=True, description="Enable browser fingerprint simulation")
|
||||
|
||||
class BrowserAutomationRequest(BaseModel):
|
||||
"""Request for browser automation service"""
|
||||
target_url: str = Field(..., description="Target URL to automate")
|
||||
action_type: str = Field(..., description="Action type: signin, extract, click")
|
||||
context_data: Optional[Dict[str, Any]] = Field(None, description="Additional context data")
|
||||
timeout_seconds: int = Field(default=30, description="Operation timeout")
|
||||
271
backend/signin_executor/app/services/signin_service.py
Normal file
271
backend/signin_executor/app/services/signin_service.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
Core sign-in business logic service
|
||||
Handles Weibo super topic sign-in operations
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import httpx
|
||||
import logging
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from app.config import settings
|
||||
from app.models.signin_models import SignInRequest, SignInResult, TaskStatus, WeiboAccount, WeiboSuperTopic, AntiBotConfig
|
||||
from app.services.weibo_client import WeiboClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SignInService:
|
||||
"""Main service for handling sign-in operations"""
|
||||
|
||||
def __init__(self):
|
||||
self.weibo_client = WeiboClient()
|
||||
self.active_tasks: Dict[str, TaskStatus] = {}
|
||||
self.antibot_config = AntiBotConfig(
|
||||
random_delay_min=settings.RANDOM_DELAY_MIN,
|
||||
random_delay_max=settings.RANDOM_DELAY_MAX,
|
||||
user_agent_rotation=settings.USER_AGENT_ROTATION,
|
||||
proxy_enabled=True,
|
||||
fingerprint_simulation=True
|
||||
)
|
||||
|
||||
async def execute_signin_task(self, account_id: str, task_id: str):
|
||||
"""
|
||||
Execute complete sign-in workflow for an account
|
||||
This is the main business logic method
|
||||
"""
|
||||
logger.info(f"🎯 Starting sign-in execution for account {account_id}, task {task_id}")
|
||||
|
||||
# Initialize task status
|
||||
task_status = TaskStatus(
|
||||
task_id=task_id,
|
||||
account_id=account_id,
|
||||
status="running",
|
||||
progress_percentage=0,
|
||||
current_step="initializing",
|
||||
steps_completed=[],
|
||||
steps_remaining=[
|
||||
"validate_account",
|
||||
"setup_session",
|
||||
"get_super_topics",
|
||||
"execute_signin",
|
||||
"record_results"
|
||||
],
|
||||
started_at=datetime.now()
|
||||
)
|
||||
self.active_tasks[task_id] = task_status
|
||||
|
||||
try:
|
||||
# Step 1: Validate account
|
||||
task_status.current_step = "validate_account"
|
||||
await self._update_task_progress(task_id, 10)
|
||||
|
||||
account = await self._get_account_info(account_id)
|
||||
if not account or account.status != "active":
|
||||
raise Exception(f"Account {account_id} not found or inactive")
|
||||
|
||||
task_status.steps_completed.append("validate_account")
|
||||
task_status.steps_remaining.remove("validate_account")
|
||||
task_status.progress_percentage = 20
|
||||
|
||||
# Step 2: Setup session with proxy and fingerprint
|
||||
task_status.current_step = "setup_session"
|
||||
await self._apply_anti_bot_protection()
|
||||
|
||||
task_status.steps_completed.append("setup_session")
|
||||
task_status.steps_remaining.remove("setup_session")
|
||||
task_status.progress_percentage = 30
|
||||
|
||||
# Step 3: Get super topics list
|
||||
task_status.current_step = "get_super_topics"
|
||||
await self._update_task_progress(task_id, 40)
|
||||
|
||||
super_topics = await self._get_super_topics_list(account)
|
||||
if not super_topics:
|
||||
logger.warning(f"No super topics found for account {account_id}")
|
||||
|
||||
task_status.steps_completed.append("get_super_topics")
|
||||
task_status.steps_remaining.remove("get_super_topics")
|
||||
task_status.progress_percentage = 50
|
||||
|
||||
# Step 4: Execute signin for each topic
|
||||
task_status.current_step = "execute_signin"
|
||||
signin_results = await self._execute_topic_signin(account, super_topics, task_id)
|
||||
|
||||
task_status.steps_completed.append("execute_signin")
|
||||
task_status.steps_remaining.remove("execute_signin")
|
||||
task_status.progress_percentage = 80
|
||||
|
||||
# Step 5: Record results
|
||||
task_status.current_step = "record_results"
|
||||
await self._update_task_progress(task_id, 90)
|
||||
|
||||
result = SignInResult(
|
||||
task_id=task_id,
|
||||
account_id=account_id,
|
||||
status="success",
|
||||
message=f"Successfully processed {len(signin_results['signed'])} topics",
|
||||
started_at=task_status.started_at,
|
||||
completed_at=datetime.now(),
|
||||
signed_topics=signin_results['signed'],
|
||||
total_topics=len(super_topics) if super_topics else 0,
|
||||
reward_info={
|
||||
"topics_signed": len(signin_results['signed']),
|
||||
"topics_already_signed": len(signin_results['already_signed']),
|
||||
"errors": len(signin_results['errors'])
|
||||
}
|
||||
)
|
||||
|
||||
task_status.status = "success"
|
||||
task_status.progress_percentage = 100
|
||||
task_status.current_step = "completed"
|
||||
|
||||
logger.info(f"✅ Sign-in task {task_id} completed successfully")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Sign-in task {task_id} failed: {e}")
|
||||
|
||||
# Update task status to failed
|
||||
if task_id in self.active_tasks:
|
||||
task_status = self.active_tasks[task_id]
|
||||
task_status.status = "failed"
|
||||
task_status.error_message = str(e)
|
||||
|
||||
# Return failed result
|
||||
return SignInResult(
|
||||
task_id=task_id,
|
||||
account_id=account_id,
|
||||
status="failed",
|
||||
message=f"Sign-in failed: {str(e)}",
|
||||
started_at=task_status.started_at if task_id in self.active_tasks else datetime.now(),
|
||||
completed_at=datetime.now(),
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
async def get_task_status(self, task_id: str) -> Optional[TaskStatus]:
|
||||
"""Get current status of a sign-in task"""
|
||||
return self.active_tasks.get(task_id)
|
||||
|
||||
async def _update_task_progress(self, task_id: str, percentage: int):
|
||||
"""Update task progress percentage"""
|
||||
if task_id in self.active_tasks:
|
||||
self.active_tasks[task_id].progress_percentage = percentage
|
||||
self.active_tasks[task_id].updated_at = datetime.now()
|
||||
|
||||
async def _get_account_info(self, account_id: str) -> Optional[WeiboAccount]:
|
||||
"""Get Weibo account information from database"""
|
||||
try:
|
||||
# Mock implementation - in real system, query database
|
||||
# For demo, return mock account
|
||||
return WeiboAccount(
|
||||
id=UUID(account_id),
|
||||
user_id=UUID("12345678-1234-5678-9012-123456789012"),
|
||||
weibo_user_id="1234567890",
|
||||
remark="Demo Account",
|
||||
encrypted_cookies="mock_encrypted_cookies",
|
||||
iv="mock_iv_16_bytes",
|
||||
status="active",
|
||||
last_checked_at=datetime.now() - timedelta(hours=1)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching account {account_id}: {e}")
|
||||
return None
|
||||
|
||||
async def _apply_anti_bot_protection(self):
|
||||
"""Apply anti-bot protection measures"""
|
||||
# Random delay to mimic human behavior
|
||||
delay = random.uniform(
|
||||
self.antibot_config.random_delay_min,
|
||||
self.antibot_config.random_delay_max
|
||||
)
|
||||
logger.debug(f"Applying random delay: {delay:.2f}s")
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
# Additional anti-bot measures would go here:
|
||||
# - User agent rotation
|
||||
# - Proxy selection
|
||||
# - Browser fingerprint simulation
|
||||
# - Request header randomization
|
||||
|
||||
async def _get_super_topics_list(self, account: WeiboAccount) -> List[WeiboSuperTopic]:
|
||||
"""Get list of super topics for account"""
|
||||
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
|
||||
)
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching super topics: {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:
|
||||
try:
|
||||
# Add small delay between requests
|
||||
await asyncio.sleep(random.uniform(0.5, 1.5))
|
||||
|
||||
if topic.is_signed:
|
||||
already_signed.append(topic.title)
|
||||
continue
|
||||
|
||||
# Execute signin for this topic
|
||||
success = 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}")
|
||||
else:
|
||||
errors.append(f"Failed to sign topic: {topic.title}")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error signing topic {topic.title}: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
errors.append(error_msg)
|
||||
|
||||
return {
|
||||
"signed": signed,
|
||||
"already_signed": already_signed,
|
||||
"errors": errors
|
||||
}
|
||||
167
backend/signin_executor/app/services/weibo_client.py
Normal file
167
backend/signin_executor/app/services/weibo_client.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Weibo API Client
|
||||
Handles all interactions with Weibo.com, including login, sign-in, and data fetching
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from app.config import settings
|
||||
from app.models.signin_models import WeiboAccount, WeiboSuperTopic
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WeiboClient:
|
||||
"""Client for interacting with Weibo API"""
|
||||
|
||||
def __init__(self):
|
||||
self.base_headers = {
|
||||
"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",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
|
||||
"Connection": "keep-alive",
|
||||
"Referer": "https://weibo.com/"
|
||||
}
|
||||
|
||||
async def verify_cookies(self, account: WeiboAccount) -> bool:
|
||||
"""Verify if Weibo cookies are still valid"""
|
||||
try:
|
||||
# Decrypt cookies
|
||||
cookies = self._decrypt_cookies(account.encrypted_cookies, account.iv)
|
||||
|
||||
async with httpx.AsyncClient(cookies=cookies, headers=self.base_headers) 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) -> bool:
|
||||
"""
|
||||
Execute sign-in for a single super topic
|
||||
"""
|
||||
try:
|
||||
# Decrypt cookies
|
||||
cookies = self._decrypt_cookies(account.encrypted_cookies, account.iv)
|
||||
|
||||
# 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": self.base_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}")
|
||||
return True
|
||||
elif response_data.get("code") == "382004":
|
||||
logger.info(f"Topic {topic.title} already signed today")
|
||||
return True # Treat as success
|
||||
else:
|
||||
logger.error(f"Failed to sign topic {topic.title}: {response_data.get('msg')}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception signing topic {topic.title}: {e}")
|
||||
return False
|
||||
|
||||
def _decrypt_cookies(self, encrypted_cookies: str, iv: str) -> Dict[str, str]:
|
||||
"""
|
||||
Decrypt cookies using AES-256-GCM
|
||||
In a real system, this would use a proper crypto library
|
||||
"""
|
||||
try:
|
||||
# Mock implementation - return dummy cookies
|
||||
return {
|
||||
"SUB": "_2A25z...",
|
||||
"SUBP": "0033Wr...",
|
||||
"ALF": "16...",
|
||||
"SSOLoginState": "16...",
|
||||
"SCF": "...",
|
||||
"UN": "testuser"
|
||||
}
|
||||
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"""
|
||||
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']}"
|
||||
}
|
||||
else:
|
||||
return None
|
||||
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"""
|
||||
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"]
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get browser fingerprint: {e}")
|
||||
return {}
|
||||
23
backend/signin_executor/requirements.txt
Normal file
23
backend/signin_executor/requirements.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
# Weibo-HotSign Sign-in Executor Service Requirements
|
||||
# Web Framework
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
|
||||
# Database
|
||||
sqlalchemy==2.0.23
|
||||
aiomysql==0.2.0
|
||||
PyMySQL==1.1.0
|
||||
redis==5.0.1
|
||||
|
||||
# Configuration
|
||||
pydantic-settings==2.0.3
|
||||
pydantic==2.5.0
|
||||
|
||||
# HTTP Client
|
||||
httpx==0.25.2
|
||||
|
||||
# Utilities
|
||||
python-dotenv==1.0.0
|
||||
|
||||
# Security (for cookie decryption)
|
||||
pycryptodome==3.19.0
|
||||
Reference in New Issue
Block a user