Files
weibo_signin/backend/signin_executor/app/services/signin_service.py

388 lines
16 KiB
Python

"""
Core sign-in business logic service
Handles Weibo super topic sign-in operations
"""
import os
import sys
import asyncio
import logging
import random
from datetime import datetime
from typing import Dict, Any, List, Optional
from uuid import UUID
from sqlalchemy import select, update
# Add parent directory to path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../.."))
from shared.models.base import AsyncSessionLocal
from shared.models.account import Account
from shared.models.signin_log import SigninLog
from shared.crypto import decrypt_cookie, derive_key
from shared.config import shared_settings
from app.config import settings
from app.models.signin_models import SignInRequest, SignInResult, TaskStatus, WeiboAccount, WeiboSuperTopic, AntiBotConfig
from app.services.weibo_client import WeiboClient
from app.services.antibot import antibot
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"
# Verify cookies before proceeding
cookies_valid = await self.weibo_client.verify_cookies(account)
if not cookies_valid:
logger.error(f"Cookies invalid for account {account_id}")
# Update account status to invalid_cookie
await self._update_account_status(account_id, "invalid_cookie")
raise Exception("Cookie validation failed - cookies are invalid or expired")
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 (replaces mock data).
Returns account dict or None if not found.
"""
try:
async with AsyncSessionLocal() as session:
stmt = select(Account).where(Account.id == account_id)
result = await session.execute(stmt)
account = result.scalar_one_or_none()
if not account:
logger.error(f"Account {account_id} not found in database")
return None
# Convert ORM model to Pydantic model
return WeiboAccount(
id=UUID(account.id),
user_id=UUID(account.user_id),
weibo_user_id=account.weibo_user_id,
remark=account.remark or "",
encrypted_cookies=account.encrypted_cookies,
iv=account.iv,
status=account.status,
last_checked_at=account.last_checked_at or datetime.now()
)
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 = antibot.get_random_delay()
logger.debug(f"Applying random delay: {delay:.2f}s")
await asyncio.sleep(delay)
# Get random User-Agent
user_agent = antibot.get_random_user_agent()
logger.debug(f"Using User-Agent: {user_agent[:50]}...")
# Try to get proxy (falls back to direct connection if unavailable)
proxy = await antibot.get_proxy()
if proxy:
logger.info(f"Using proxy for requests")
else:
logger.info("Using direct connection (no proxy available)")
# Get browser fingerprint
fingerprint = antibot.get_fingerprint_data()
logger.debug(f"Browser fingerprint: {fingerprint}")
async def _get_super_topics_list(self, account: WeiboAccount) -> List[WeiboSuperTopic]:
"""
Fetch the real list of followed super topics from Weibo API.
Delegates to WeiboClient.get_super_topics().
"""
try:
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 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 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:
if topic.is_signed:
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="Already signed today",
)
continue
# 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,
)
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=last_error,
)
except Exception as e:
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),
)
return {
"signed": signed,
"already_signed": already_signed,
"errors": errors,
}
async def _write_signin_log(
self,
account_id: str,
topic_title: str,
status: str,
reward_info: Optional[Dict[str, Any]],
error_message: Optional[str]
):
"""
Write signin result to signin_logs table.
Replaces mock implementation with real database write.
"""
try:
async with AsyncSessionLocal() as session:
log = SigninLog(
account_id=account_id,
topic_title=topic_title,
status=status,
reward_info=reward_info,
error_message=error_message,
)
session.add(log)
await session.commit()
logger.debug(f"Wrote signin log for account {account_id}, topic {topic_title}, status {status}")
except Exception as e:
logger.error(f"Failed to write signin log: {e}")
async def _update_account_status(self, account_id: str, status: str):
"""
Update account status in database.
Used when cookie is invalid or account is banned.
"""
try:
async with AsyncSessionLocal() as session:
stmt = (
update(Account)
.where(Account.id == account_id)
.values(status=status, last_checked_at=datetime.now())
)
await session.execute(stmt)
await session.commit()
logger.info(f"Updated account {account_id} status to {status}")
except Exception as e:
logger.error(f"Failed to update account status: {e}")