Files
weibo_signin/frontend/app.py
2026-03-09 16:10:29 +08:00

709 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify
from flask_session import Session
import requests
from functools import wraps
from dotenv import load_dotenv
from datetime import datetime
load_dotenv()
app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-secret-key')
app.config['SESSION_TYPE'] = 'filesystem'
Session(app)
API_BASE_URL = os.getenv('API_BASE_URL', 'http://localhost:8000')
AUTH_BASE_URL = os.getenv('AUTH_BASE_URL', 'http://localhost:8001')
def get_headers():
"""获取请求头,包含认证令牌"""
headers = {'Content-Type': 'application/json'}
if 'access_token' in session:
headers['Authorization'] = f"Bearer {session['access_token']}"
return headers
def login_required(f):
"""登录验证装饰器"""
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user' not in session:
flash('Please login first', 'warning')
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
@app.route('/')
def index():
if 'user' in session:
return redirect(url_for('dashboard'))
return redirect(url_for('login'))
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
if password != confirm_password:
flash('Passwords do not match', 'danger')
return redirect(url_for('register'))
try:
response = requests.post(
f'{AUTH_BASE_URL}/auth/register',
json={'username': username, 'email': email, 'password': password},
timeout=10
)
if response.status_code == 201:
data = response.json()
session['user'] = data['user']
session['access_token'] = data['access_token']
session['refresh_token'] = data['refresh_token']
flash('Registration successful!', 'success')
return redirect(url_for('dashboard'))
else:
error_data = response.json()
flash(error_data.get('detail', 'Registration failed'), 'danger')
except requests.RequestException as e:
flash(f'Connection error: {str(e)}', 'danger')
return render_template('register.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
try:
response = requests.post(
f'{AUTH_BASE_URL}/auth/login',
json={'email': email, 'password': password},
timeout=10
)
if response.status_code == 200:
data = response.json()
session['user'] = data['user']
session['access_token'] = data['access_token']
session['refresh_token'] = data['refresh_token']
flash('Login successful!', 'success')
return redirect(url_for('dashboard'))
else:
error_data = response.json()
flash(error_data.get('detail', 'Login failed'), 'danger')
except requests.RequestException as e:
flash(f'Connection error: {str(e)}', 'danger')
return render_template('login.html')
@app.route('/logout')
def logout():
session.clear()
flash('Logged out successfully', 'success')
return redirect(url_for('login'))
@app.route('/dashboard')
@login_required
def dashboard():
try:
response = requests.get(
f'{API_BASE_URL}/api/v1/accounts',
headers=get_headers(),
timeout=10
)
data = response.json()
accounts = data.get('data', []) if data.get('success') else []
except requests.RequestException:
accounts = []
flash('Failed to load accounts', 'warning')
return render_template('dashboard.html', accounts=accounts, user=session.get('user'))
@app.route('/accounts/new', methods=['GET', 'POST'])
@login_required
def add_account():
if request.method == 'POST':
login_method = request.form.get('login_method', 'manual')
if login_method == 'manual':
weibo_user_id = request.form.get('weibo_user_id')
cookie = request.form.get('cookie')
remark = request.form.get('remark')
try:
response = requests.post(
f'{API_BASE_URL}/api/v1/accounts',
json={
'weibo_user_id': weibo_user_id,
'cookie': cookie,
'remark': remark
},
headers=get_headers(),
timeout=10
)
data = response.json()
if response.status_code == 200 and data.get('success'):
flash('Account added successfully!', 'success')
return redirect(url_for('dashboard'))
else:
flash(data.get('message', 'Failed to add account'), 'danger')
except requests.RequestException as e:
flash(f'Connection error: {str(e)}', 'danger')
# 扫码授权功能总是启用(使用微博网页版接口)
weibo_qrcode_enabled = True
return render_template('add_account.html',
weibo_qrcode_enabled=weibo_qrcode_enabled)
@app.route('/api/weibo/qrcode/generate', methods=['POST'])
@login_required
def generate_weibo_qrcode():
"""生成微博扫码登录二维码(模拟网页版)"""
import uuid
import time
import traceback
try:
# 模拟微博网页版的二维码生成接口
# 实际接口https://login.sina.com.cn/sso/qrcode/image
# 生成唯一的 qrcode_id
qrcode_id = str(uuid.uuid4())
# 调用微博的二维码生成接口
qr_api_url = 'https://login.sina.com.cn/sso/qrcode/image'
params = {
'entry': 'weibo',
'size': '180',
'callback': f'STK_{int(time.time() * 1000)}'
}
# 添加浏览器请求头,模拟真实浏览器
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',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive'
}
print(f"[DEBUG] 请求微博 API: {qr_api_url}")
response = requests.get(qr_api_url, params=params, headers=headers, timeout=10)
print(f"[DEBUG] 响应状态码: {response.status_code}")
print(f"[DEBUG] 响应内容: {response.text[:200]}")
# 微博返回的是 JSONP 格式,需要解析
# 格式STK_xxx({"retcode":20000000,"qrid":"xxx","image":"data:image/png;base64,xxx"})
import re
import json
match = re.search(r'\((.*)\)', response.text)
if match:
data = json.loads(match.group(1))
print(f"[DEBUG] 解析的数据: retcode={data.get('retcode')}, data={data.get('data')}")
if data.get('retcode') == 20000000:
# 微博返回的数据结构:{"retcode":20000000,"data":{"qrid":"...","image":"..."}}
qr_data = data.get('data', {})
qrid = qr_data.get('qrid')
qr_image_url = qr_data.get('image')
if not qrid or not qr_image_url:
print(f"[ERROR] 缺少 qrid 或 image: qrid={qrid}, image={qr_image_url}")
return jsonify({'success': False, 'error': '二维码数据不完整'}), 500
# 如果 image 是相对 URL补全为完整 URL
if qr_image_url.startswith('//'):
qr_image_url = 'https:' + qr_image_url
print(f"[DEBUG] 二维码 URL: {qr_image_url}")
# 存储二维码状态
if 'weibo_qrcodes' not in session:
session['weibo_qrcodes'] = {}
session['weibo_qrcodes'][qrid] = {
'status': 'waiting',
'created_at': str(datetime.now()),
'qrcode_id': qrcode_id
}
session.modified = True
print(f"[DEBUG] 二维码生成成功: qrid={qrid}")
return jsonify({
'success': True,
'qrid': qrid,
'qr_image': qr_image_url, # 返回二维码图片 URL
'expires_in': 180
})
print("[DEBUG] 未能解析响应或 retcode 不正确")
return jsonify({'success': False, 'error': '生成二维码失败'}), 500
except Exception as e:
print(f"[ERROR] 生成二维码异常: {str(e)}")
print(f"[ERROR] 堆栈跟踪:\n{traceback.format_exc()}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/weibo/qrcode/check/<qrid>', methods=['GET'])
@login_required
def check_weibo_qrcode(qrid):
"""检查微博扫码状态(模拟网页版)"""
import time
try:
# 检查二维码是否存在
qrcodes = session.get('weibo_qrcodes', {})
if qrid not in qrcodes:
return jsonify({'status': 'expired'})
# 调用微博的轮询接口
# 实际接口https://login.sina.com.cn/sso/qrcode/check
check_api_url = 'https://login.sina.com.cn/sso/qrcode/check'
params = {
'entry': 'weibo',
'qrid': qrid,
'callback': f'STK_{int(time.time() * 1000)}'
}
# 添加浏览器请求头
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',
'Connection': 'keep-alive'
}
response = requests.get(check_api_url, params=params, headers=headers, timeout=10)
print(f"[DEBUG] 检查状态 - qrid: {qrid}")
print(f"[DEBUG] 检查状态 - 响应状态码: {response.status_code}")
print(f"[DEBUG] 检查状态 - 响应内容: {response.text[:500]}")
# 解析 JSONP 响应
import re
match = re.search(r'\((.*)\)', response.text)
if match:
import json
data = json.loads(match.group(1))
retcode = data.get('retcode')
print(f"[DEBUG] 检查状态 - retcode: {retcode}, data: {data}")
# 微博扫码状态码:
# 20000000: 等待扫码
# 50050001: 已扫码,等待确认
# 20000001: 确认成功
# 50050002: 二维码过期
# 50050004: 取消授权
# 50114001: 未使用(等待扫码)
# 50114004: 该二维码已登录(可能是成功状态)
if retcode == 20000000 or retcode == 50114001:
# 等待扫码
return jsonify({'status': 'waiting'})
elif retcode == 50050001:
# 已扫码,等待确认
return jsonify({'status': 'scanned'})
elif retcode == 20000001 or retcode == 50114004:
# 登录成功,获取跳转 URL
# 50114004 也表示已登录成功
alt_url = data.get('alt')
# 如果没有 alt 字段,尝试从 data 中获取
if not alt_url and data.get('data'):
alt_url = data.get('data', {}).get('alt')
print(f"[DEBUG] 登录成功 - retcode: {retcode}, alt_url: {alt_url}, full_data: {data}")
# 如果没有 alt_url尝试构造登录 URL
if not alt_url:
# 尝试使用 qrid 构造登录 URL
# 微博可能使用不同的 URL 格式
possible_urls = [
f"https://login.sina.com.cn/sso/login.php?entry=weibo&qrid={qrid}",
f"https://passport.weibo.com/sso/login?qrid={qrid}",
f"https://login.sina.com.cn/sso/qrcode/login?qrid={qrid}"
]
print(f"[DEBUG] 尝试构造登录 URL")
for url in possible_urls:
try:
print(f"[DEBUG] 尝试 URL: {url}")
test_response = requests.get(url, headers=headers, allow_redirects=False, timeout=5)
print(f"[DEBUG] 响应状态码: {test_response.status_code}")
if test_response.status_code in [200, 302, 301]:
alt_url = url
print(f"[DEBUG] 找到有效 URL: {alt_url}")
break
except Exception as e:
print(f"[DEBUG] URL 失败: {str(e)}")
continue
if alt_url:
# 添加浏览器请求头
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': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Connection': 'keep-alive'
}
print(f"[DEBUG] 访问跳转 URL: {alt_url}")
# 访问跳转 URL 获取 Cookie
cookie_response = requests.get(alt_url, headers=headers, allow_redirects=True, timeout=10)
cookies = cookie_response.cookies
print(f"[DEBUG] 获取到的 Cookies: {dict(cookies)}")
# 构建 Cookie 字符串
cookie_str = '; '.join([f'{k}={v}' for k, v in cookies.items()])
if not cookie_str:
print("[ERROR] 未获取到任何 Cookie")
return jsonify({'status': 'error', 'error': '未获取到 Cookie'})
print(f"[DEBUG] Cookie 字符串长度: {len(cookie_str)}")
# 获取用户信息
# 可以通过 Cookie 访问微博 API 获取 uid
user_info_url = 'https://weibo.com/ajax/profile/info'
user_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': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
}
print(f"[DEBUG] 请求用户信息: {user_info_url}")
user_response = requests.get(user_info_url, cookies=cookies, headers=user_headers, timeout=10)
print(f"[DEBUG] 用户信息响应状态码: {user_response.status_code}")
print(f"[DEBUG] 用户信息响应内容: {user_response.text[:500]}")
user_data = user_response.json()
if user_data.get('ok') == 1:
user_info = user_data.get('data', {}).get('user', {})
weibo_uid = user_info.get('idstr', '')
screen_name = user_info.get('screen_name', 'Weibo User')
print(f"[DEBUG] 获取用户信息成功: uid={weibo_uid}, name={screen_name}")
# 更新状态
session['weibo_qrcodes'][qrid]['status'] = 'success'
session['weibo_qrcodes'][qrid]['cookie'] = cookie_str
session['weibo_qrcodes'][qrid]['weibo_uid'] = weibo_uid
session['weibo_qrcodes'][qrid]['screen_name'] = screen_name
session.modified = True
return jsonify({
'status': 'success',
'weibo_uid': weibo_uid,
'screen_name': screen_name
})
else:
print(f"[ERROR] 获取用户信息失败: {user_data}")
return jsonify({'status': 'error', 'error': '获取用户信息失败'})
else:
print("[ERROR] 未获取到跳转 URL")
return jsonify({'status': 'error', 'error': '获取登录信息失败'})
elif retcode == 50050002:
return jsonify({'status': 'expired'})
elif retcode == 50050004:
return jsonify({'status': 'cancelled'})
else:
# 未知状态码,记录日志
print(f"[WARN] 未知的 retcode: {retcode}, msg: {data.get('msg')}")
return jsonify({'status': 'waiting'}) # 默认继续等待
print("[DEBUG] 未能解析响应")
return jsonify({'status': 'error', 'error': '检查状态失败'})
except Exception as e:
print(f"[ERROR] 检查二维码状态异常: {str(e)}")
import traceback
print(f"[ERROR] 堆栈跟踪:\n{traceback.format_exc()}")
return jsonify({'status': 'error', 'error': str(e)})
@app.route('/api/weibo/qrcode/add-account', methods=['POST'])
@login_required
def add_account_from_qrcode():
"""从扫码结果添加账号"""
try:
data = request.json
qrid = data.get('qrid')
remark = data.get('remark', '')
print(f"[DEBUG] 添加账号 - qrid: {qrid}")
# 获取扫码结果
qrcodes = session.get('weibo_qrcodes', {})
qr_info = qrcodes.get(qrid)
print(f"[DEBUG] 添加账号 - qr_info: {qr_info}")
if not qr_info or qr_info.get('status') != 'success':
print(f"[ERROR] 添加账号失败 - 二维码状态不正确: {qr_info.get('status') if qr_info else 'None'}")
return jsonify({'success': False, 'message': '二维码未完成授权'}), 400
cookie = qr_info.get('cookie')
weibo_uid = qr_info.get('weibo_uid')
screen_name = qr_info.get('screen_name', 'Weibo User')
print(f"[DEBUG] 添加账号 - uid: {weibo_uid}, name: {screen_name}, cookie_len: {len(cookie) if cookie else 0}")
if not remark:
remark = f"{screen_name} (扫码添加)"
# 添加账号到系统
print(f"[DEBUG] 调用后端 API 添加账号: {API_BASE_URL}/api/v1/accounts")
response = requests.post(
f'{API_BASE_URL}/api/v1/accounts',
json={
'weibo_user_id': weibo_uid,
'cookie': cookie,
'remark': remark
},
headers=get_headers(),
timeout=10
)
print(f"[DEBUG] 后端响应状态码: {response.status_code}")
print(f"[DEBUG] 后端响应内容: {response.text[:500]}")
result = response.json()
if response.status_code == 200 and result.get('success'):
# 清除已使用的二维码
session['weibo_qrcodes'].pop(qrid, None)
session.modified = True
print(f"[DEBUG] 账号添加成功")
return jsonify({
'success': True,
'message': 'Account added successfully',
'account': result.get('data', {})
})
else:
print(f"[ERROR] 后端返回失败: {result}")
return jsonify({
'success': False,
'message': result.get('message', 'Failed to add account')
}), 400
except Exception as e:
print(f"[ERROR] 添加账号异常: {str(e)}")
import traceback
print(f"[ERROR] 堆栈跟踪:\n{traceback.format_exc()}")
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/accounts/<account_id>')
@login_required
def account_detail(account_id):
try:
# 获取账号详情
response = requests.get(
f'{API_BASE_URL}/api/v1/accounts/{account_id}',
headers=get_headers(),
timeout=10
)
account_data = response.json()
account = account_data.get('data') if account_data.get('success') else None
# 获取任务列表
tasks_response = requests.get(
f'{API_BASE_URL}/api/v1/accounts/{account_id}/tasks',
headers=get_headers(),
timeout=10
)
tasks_data = tasks_response.json()
tasks = tasks_data.get('data', []) if tasks_data.get('success') else []
# 获取签到日志
page = request.args.get('page', 1, type=int)
logs_response = requests.get(
f'{API_BASE_URL}/api/v1/accounts/{account_id}/signin-logs',
params={'page': page, 'size': 20},
headers=get_headers(),
timeout=10
)
logs_data = logs_response.json()
logs = logs_data.get('data', {}) if logs_data.get('success') else {}
if not account:
flash('Account not found', 'danger')
return redirect(url_for('dashboard'))
return render_template(
'account_detail.html',
account=account,
tasks=tasks,
logs=logs,
user=session.get('user')
)
except requests.RequestException as e:
flash(f'Connection error: {str(e)}', 'danger')
return redirect(url_for('dashboard'))
@app.route('/accounts/<account_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_account(account_id):
if request.method == 'POST':
remark = request.form.get('remark')
cookie = request.form.get('cookie')
try:
data = {'remark': remark}
if cookie:
data['cookie'] = cookie
response = requests.put(
f'{API_BASE_URL}/api/v1/accounts/{account_id}',
json=data,
headers=get_headers(),
timeout=10
)
result = response.json()
if response.status_code == 200 and result.get('success'):
flash('Account updated successfully!', 'success')
return redirect(url_for('account_detail', account_id=account_id))
else:
flash(result.get('message', 'Failed to update account'), 'danger')
except requests.RequestException as e:
flash(f'Connection error: {str(e)}', 'danger')
try:
response = requests.get(
f'{API_BASE_URL}/api/v1/accounts/{account_id}',
headers=get_headers(),
timeout=10
)
data = response.json()
account = data.get('data') if data.get('success') else None
if not account:
flash('Account not found', 'danger')
return redirect(url_for('dashboard'))
return render_template('edit_account.html', account=account)
except requests.RequestException as e:
flash(f'Connection error: {str(e)}', 'danger')
return redirect(url_for('dashboard'))
@app.route('/accounts/<account_id>/delete', methods=['POST'])
@login_required
def delete_account(account_id):
try:
response = requests.delete(
f'{API_BASE_URL}/api/v1/accounts/{account_id}',
headers=get_headers(),
timeout=10
)
data = response.json()
if response.status_code == 200 and data.get('success'):
flash('Account deleted successfully!', 'success')
else:
flash(data.get('message', 'Failed to delete account'), 'danger')
except requests.RequestException as e:
flash(f'Connection error: {str(e)}', 'danger')
return redirect(url_for('dashboard'))
@app.route('/accounts/<account_id>/tasks/new', methods=['GET', 'POST'])
@login_required
def add_task(account_id):
if request.method == 'POST':
cron_expression = request.form.get('cron_expression')
try:
response = requests.post(
f'{API_BASE_URL}/api/v1/accounts/{account_id}/tasks',
json={'cron_expression': cron_expression},
headers=get_headers(),
timeout=10
)
data = response.json()
if response.status_code == 200 and data.get('success'):
flash('Task created successfully!', 'success')
return redirect(url_for('account_detail', account_id=account_id))
else:
flash(data.get('message', 'Failed to create task'), 'danger')
except requests.RequestException as e:
flash(f'Connection error: {str(e)}', 'danger')
return render_template('add_task.html', account_id=account_id)
@app.route('/tasks/<task_id>/toggle', methods=['POST'])
@login_required
def toggle_task(task_id):
is_enabled = request.form.get('is_enabled') == 'true'
try:
response = requests.put(
f'{API_BASE_URL}/api/v1/tasks/{task_id}',
json={'is_enabled': not is_enabled},
headers=get_headers(),
timeout=10
)
data = response.json()
if response.status_code == 200 and data.get('success'):
flash('Task updated successfully!', 'success')
else:
flash(data.get('message', 'Failed to update task'), 'danger')
except requests.RequestException as e:
flash(f'Connection error: {str(e)}', 'danger')
account_id = request.form.get('account_id')
return redirect(url_for('account_detail', account_id=account_id))
@app.route('/tasks/<task_id>/delete', methods=['POST'])
@login_required
def delete_task(task_id):
account_id = request.form.get('account_id')
try:
response = requests.delete(
f'{API_BASE_URL}/api/v1/tasks/{task_id}',
headers=get_headers(),
timeout=10
)
data = response.json()
if response.status_code == 200 and data.get('success'):
flash('Task deleted successfully!', 'success')
else:
flash(data.get('message', 'Failed to delete task'), 'danger')
except requests.RequestException as e:
flash(f'Connection error: {str(e)}', 'danger')
return redirect(url_for('account_detail', account_id=account_id))
@app.errorhandler(404)
def not_found(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def server_error(error):
return render_template('500.html'), 500
if __name__ == '__main__':
app.run(debug=True, port=5000)