mirror of
https://github.com/halfwaystudent/douyin-sparkflow.git
synced 2026-08-28 19:47:03 +08:00
754 lines
29 KiB
Python
754 lines
29 KiB
Python
import json
|
|
import logging
|
|
import traceback
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
import uvicorn
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse, Response
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.templating import Jinja2Templates
|
|
from starlette.middleware.sessions import SessionMiddleware
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
from core.friends import fetch_account_friends
|
|
from core.tasks import run_browser_tasks
|
|
from utils.config import (
|
|
get_app_settings,
|
|
get_config,
|
|
get_userData,
|
|
normalize_unique_id,
|
|
save_app_settings,
|
|
save_config,
|
|
save_userData,
|
|
upsert_user_account,
|
|
)
|
|
from webui.auth import (
|
|
bootstrap_admin_password,
|
|
clear_session,
|
|
csrf_token,
|
|
current_user,
|
|
is_bootstrapped,
|
|
is_https_request,
|
|
issue_session,
|
|
update_admin_password,
|
|
validate_csrf,
|
|
verify_password,
|
|
)
|
|
from webui.ops import (
|
|
get_ops_snapshot,
|
|
read_log_tail,
|
|
refresh_proxy,
|
|
restart_proxy,
|
|
run_task_now,
|
|
run_unsent_retry_now,
|
|
update_daily_schedule,
|
|
)
|
|
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent
|
|
TEMPLATES_DIR = BASE_DIR / "templates"
|
|
STATIC_DIR = BASE_DIR / "static"
|
|
DEBUG_ARTIFACTS_DIR = BASE_DIR.parent / "logs" / "debug_artifacts"
|
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
|
|
|
|
|
def _dedupe_targets(values):
|
|
seen = set()
|
|
result = []
|
|
for value in values:
|
|
normalized = str(value).strip()
|
|
if not normalized or normalized in seen:
|
|
continue
|
|
seen.add(normalized)
|
|
result.append(normalized)
|
|
return result
|
|
|
|
|
|
def _split_target_entries(values):
|
|
expanded = []
|
|
for value in values:
|
|
raw = str(value).replace(",", "\n")
|
|
expanded.extend(raw.splitlines())
|
|
return _dedupe_targets(expanded)
|
|
|
|
|
|
def extract_targets_from_form(form):
|
|
if hasattr(form, "getlist"):
|
|
checkbox_targets = _split_target_entries(form.getlist("targets"))
|
|
if checkbox_targets:
|
|
return checkbox_targets
|
|
raw_targets = str(form.get("targets", ""))
|
|
return _split_target_entries([raw_targets])
|
|
|
|
|
|
def find_account(accounts, unique_id):
|
|
normalized = normalize_unique_id(unique_id)
|
|
for account in accounts:
|
|
if normalize_unique_id(account.get("unique_id")) == normalized:
|
|
return account
|
|
return None
|
|
|
|
|
|
def is_account_enabled(account):
|
|
return bool(account.get("enabled", True))
|
|
|
|
|
|
def coerce_int(value, default, minimum=0):
|
|
try:
|
|
return max(minimum, int(str(value).strip()))
|
|
except (TypeError, ValueError):
|
|
return max(minimum, int(default))
|
|
|
|
|
|
def _schedule_timezone():
|
|
return timezone(timedelta(hours=8), name="Asia/Shanghai")
|
|
|
|
|
|
def _parse_sent_at(raw_value):
|
|
if not raw_value:
|
|
return None
|
|
raw = str(raw_value).strip()
|
|
if raw.endswith("Z"):
|
|
raw = raw[:-1] + "+00:00"
|
|
try:
|
|
parsed = datetime.fromisoformat(raw)
|
|
except ValueError:
|
|
return None
|
|
if parsed.tzinfo is None:
|
|
return parsed.replace(tzinfo=_schedule_timezone())
|
|
return parsed.astimezone(_schedule_timezone())
|
|
|
|
|
|
def _target_sent_today(account, target_name):
|
|
entry = dict(account.get("message_history") or {}).get(target_name) or {}
|
|
sent_at = _parse_sent_at(entry.get("sentAt"))
|
|
return bool(sent_at and sent_at.date() == datetime.now(_schedule_timezone()).date())
|
|
|
|
|
|
def login_desktop_api_url():
|
|
settings = get_app_settings(force_reload=True)
|
|
return str(settings.get("login_desktop_api_url") or "http://127.0.0.1:18090").rstrip("/")
|
|
|
|
|
|
def login_desktop_public_url(request: Request) -> str:
|
|
host = request.url.hostname or "127.0.0.1"
|
|
scheme = request.url.scheme or "http"
|
|
return f"{scheme}://{host}:8788/vnc.html?autoconnect=1&resize=scale&view_only=0"
|
|
|
|
|
|
def call_login_desktop(path: str, *, method: str = "GET", payload: dict | None = None, timeout: int = 20) -> dict:
|
|
url = f"{login_desktop_api_url()}{path}"
|
|
data = None
|
|
headers = {}
|
|
if payload is not None:
|
|
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
headers["Content-Type"] = "application/json; charset=utf-8"
|
|
request = urllib.request.Request(url, method=method, data=data, headers=headers)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
body = response.read().decode("utf-8", errors="replace")
|
|
return json.loads(body) if body.strip() else {}
|
|
except urllib.error.HTTPError as exc:
|
|
body = exc.read().decode("utf-8", errors="replace")
|
|
raise RuntimeError(f"login-desktop API error {exc.code}: {body}") from exc
|
|
except urllib.error.URLError as exc:
|
|
raise RuntimeError(f"login-desktop unavailable: {exc.reason}") from exc
|
|
|
|
|
|
def save_exported_login_result(login_result: dict, *, relogin_unique_id: str = "", display_name: str = "") -> tuple[dict, str]:
|
|
unique_id = normalize_unique_id(login_result.get("unique_id"))
|
|
username = str(display_name or login_result.get("username") or "").strip()
|
|
cookies = list(login_result.get("cookies") or [])
|
|
if not unique_id or not username or not cookies:
|
|
raise RuntimeError("Exported login result is incomplete")
|
|
|
|
accounts = get_userData(force_reload=True)
|
|
|
|
if relogin_unique_id:
|
|
target = find_account(accounts, relogin_unique_id)
|
|
if not target:
|
|
raise RuntimeError("Target account not found for relogin")
|
|
target["unique_id"] = unique_id
|
|
target["username"] = username
|
|
target["cookies"] = cookies
|
|
target.setdefault("enabled", True)
|
|
save_userData(accounts)
|
|
return target, "updated"
|
|
|
|
existing = find_account(accounts, unique_id)
|
|
if existing:
|
|
existing["username"] = username
|
|
existing["cookies"] = cookies
|
|
existing.setdefault("enabled", True)
|
|
save_userData(accounts)
|
|
return existing, "updated"
|
|
|
|
account = upsert_user_account(unique_id, username, cookies, [])
|
|
return account, "created"
|
|
|
|
|
|
def create_app():
|
|
settings = get_app_settings()
|
|
app = FastAPI(title="DouYin Spark Flow Admin")
|
|
app.add_middleware(
|
|
SessionMiddleware,
|
|
secret_key=settings["session_secret"],
|
|
max_age=settings["session_max_age_seconds"],
|
|
same_site="lax",
|
|
https_only=False,
|
|
)
|
|
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|
DEBUG_ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
app.mount("/debug-artifacts", StaticFiles(directory=str(DEBUG_ARTIFACTS_DIR)), name="debug-artifacts")
|
|
|
|
@app.exception_handler(Exception)
|
|
async def global_exception_handler(request: Request, exc: Exception):
|
|
tb = traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
tb_text = "".join(tb)
|
|
logger.error("Unhandled exception on %s %s:\n%s", request.method, request.url.path, tb_text)
|
|
return PlainTextResponse(f"Internal Server Error\n\n{tb_text}", status_code=500)
|
|
|
|
def render_template(request, template_name, context=None, status_code=200):
|
|
base_context = context or {}
|
|
base_context.update(
|
|
{
|
|
"request": request,
|
|
"current_user": current_user(request),
|
|
"csrf_token": csrf_token(request) if current_user(request) else "",
|
|
"is_https": is_https_request(request),
|
|
"app_settings": get_app_settings(force_reload=True),
|
|
"login_desktop_public_url": login_desktop_public_url(request),
|
|
}
|
|
)
|
|
return templates.TemplateResponse(request, template_name, base_context, status_code=status_code)
|
|
|
|
def redirect(path="/", status_code=303):
|
|
return RedirectResponse(url=path, status_code=status_code)
|
|
|
|
def require_user(request):
|
|
if not current_user(request):
|
|
return redirect("/login")
|
|
return None
|
|
|
|
def flash(request, message, level="info"):
|
|
request.session["flash"] = {"message": message, "level": level}
|
|
|
|
def pop_flash(request):
|
|
return request.session.pop("flash", None)
|
|
|
|
@app.get("/login", response_class=HTMLResponse)
|
|
async def login_page(request: Request):
|
|
if current_user(request):
|
|
return redirect("/")
|
|
return render_template(
|
|
request,
|
|
"login.html",
|
|
{
|
|
"flash": pop_flash(request),
|
|
"bootstrapped": is_bootstrapped(),
|
|
},
|
|
)
|
|
|
|
@app.post("/bootstrap")
|
|
async def bootstrap(request: Request):
|
|
if is_bootstrapped():
|
|
flash(request, "Admin login is already configured.", "warning")
|
|
return redirect("/login")
|
|
|
|
form = await request.form()
|
|
username = str(form.get("username", "admin")).strip() or "admin"
|
|
password = str(form.get("password", ""))
|
|
confirm = str(form.get("confirm_password", ""))
|
|
if not password or password != confirm:
|
|
flash(request, "Password setup failed. Please enter matching passwords.", "error")
|
|
return redirect("/login")
|
|
|
|
bootstrap_admin_password(password, username=username)
|
|
flash(request, "Admin credentials created. Please log in.", "success")
|
|
return redirect("/login")
|
|
|
|
@app.post("/login")
|
|
async def login_action(request: Request):
|
|
if not is_bootstrapped():
|
|
flash(request, "Create the admin password first.", "warning")
|
|
return redirect("/login")
|
|
|
|
form = await request.form()
|
|
username = str(form.get("username", "")).strip()
|
|
password = str(form.get("password", ""))
|
|
settings = get_app_settings(force_reload=True)
|
|
if username != settings["admin_username"] or not verify_password(password, settings["admin_password_hash"]):
|
|
flash(request, "Invalid username or password.", "error")
|
|
return redirect("/login")
|
|
|
|
issue_session(request, username)
|
|
flash(request, "Signed in successfully.", "success")
|
|
return redirect("/")
|
|
|
|
@app.post("/logout")
|
|
async def logout_action(request: Request):
|
|
clear_session(request)
|
|
return redirect("/login")
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def dashboard(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
return render_template(
|
|
request,
|
|
"dashboard.html",
|
|
{
|
|
"flash": pop_flash(request),
|
|
"accounts": get_userData(force_reload=True),
|
|
"runtime_config": get_config(force_reload=True),
|
|
"ops": get_ops_snapshot(),
|
|
},
|
|
)
|
|
|
|
@app.get("/ops/send-console", response_class=HTMLResponse)
|
|
async def send_console_page(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
return render_template(
|
|
request,
|
|
"send_console.html",
|
|
{
|
|
"flash": pop_flash(request),
|
|
"ops": get_ops_snapshot(),
|
|
},
|
|
)
|
|
|
|
@app.post("/accounts/{unique_id}/update")
|
|
async def update_account(request: Request, unique_id: str):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return Response("Invalid CSRF token", status_code=403)
|
|
|
|
username = str(form.get("username", "")).strip()
|
|
targets = extract_targets_from_form(form)
|
|
|
|
accounts = get_userData(force_reload=True)
|
|
account = find_account(accounts, unique_id)
|
|
if account:
|
|
account["username"] = username or account.get("username", "")
|
|
account["targets"] = targets
|
|
account["enabled"] = str(form.get("enabled", "")) == "on"
|
|
save_userData(accounts)
|
|
flash(request, f"Updated account {account['username']}.", "success")
|
|
else:
|
|
flash(request, "Account not found.", "error")
|
|
|
|
return redirect("/")
|
|
|
|
@app.post("/accounts/{unique_id}/toggle-enabled")
|
|
async def toggle_account_enabled(request: Request, unique_id: str):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return Response("Invalid CSRF token", status_code=403)
|
|
|
|
accounts = get_userData(force_reload=True)
|
|
account = find_account(accounts, unique_id)
|
|
if not account:
|
|
flash(request, "Account not found.", "error")
|
|
return redirect("/")
|
|
|
|
account["enabled"] = not is_account_enabled(account)
|
|
save_userData(accounts)
|
|
flash(
|
|
request,
|
|
f"{account.get('username', 'Account')} 已{'启用' if account['enabled'] else '停用'}自动续火花。",
|
|
"success",
|
|
)
|
|
return redirect("/")
|
|
|
|
@app.post("/accounts/{unique_id}/friends/refresh")
|
|
async def refresh_account_friend_list(request: Request, unique_id: str):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return JSONResponse({"error": "Unauthorized"}, status_code=401)
|
|
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return JSONResponse({"error": "Invalid CSRF token"}, status_code=403)
|
|
|
|
accounts = get_userData(force_reload=True)
|
|
account = find_account(accounts, unique_id)
|
|
if not account:
|
|
return JSONResponse({"error": "Account not found."}, status_code=404)
|
|
|
|
try:
|
|
friends = await fetch_account_friends(account)
|
|
account["friends_cache"] = friends
|
|
account["friends_cache_updated_at"] = datetime.now().isoformat(timespec="seconds")
|
|
save_userData(accounts)
|
|
return JSONResponse(
|
|
{
|
|
"friends": friends,
|
|
"updated_at": account["friends_cache_updated_at"],
|
|
"message": f"已刷新 {len(friends)} 个好友",
|
|
}
|
|
)
|
|
except RuntimeError as exc:
|
|
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
|
|
@app.post("/accounts/{unique_id}/delete")
|
|
async def delete_account(request: Request, unique_id: str):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return Response("Invalid CSRF token", status_code=403)
|
|
|
|
accounts = get_userData(force_reload=True)
|
|
updated_accounts = [item for item in accounts if normalize_unique_id(item.get("unique_id")) != normalize_unique_id(unique_id)]
|
|
if len(updated_accounts) != len(accounts):
|
|
save_userData(updated_accounts)
|
|
flash(request, "Account deleted.", "success")
|
|
else:
|
|
flash(request, "Account not found.", "error")
|
|
return redirect("/")
|
|
|
|
@app.post("/accounts/{unique_id}/retry-target")
|
|
async def retry_account_target(request: Request, unique_id: str):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return Response("Invalid CSRF token", status_code=403)
|
|
|
|
target_name = str(form.get("target", "")).strip()
|
|
if not target_name:
|
|
flash(request, "Target is required for retry.", "error")
|
|
return redirect("/ops/send-console")
|
|
|
|
accounts = get_userData(force_reload=True)
|
|
account = find_account(accounts, unique_id)
|
|
if not account:
|
|
flash(request, "Account not found.", "error")
|
|
return redirect("/ops/send-console")
|
|
|
|
account_copy = dict(account)
|
|
account_copy["targets"] = [target_name]
|
|
config = get_config(force_reload=True)
|
|
config["taskCount"] = 1
|
|
|
|
try:
|
|
await run_browser_tasks(config, [account_copy])
|
|
except Exception as exc:
|
|
flash(request, f"Retry failed for {account.get('username', 'Account')} / {target_name}: {exc}", "error")
|
|
return redirect("/ops/send-console")
|
|
|
|
updated_account = find_account(get_userData(force_reload=True), unique_id) or {}
|
|
if _target_sent_today(updated_account, target_name):
|
|
flash(request, f"Retried {account.get('username', 'Account')} / {target_name} successfully.", "success")
|
|
else:
|
|
failure_entry = dict(updated_account.get("failure_queue") or {}).get(target_name) or {}
|
|
reason = str(failure_entry.get("reason") or "Retry did not confirm a successful send.")
|
|
flash(request, f"Retry did not succeed for {account.get('username', 'Account')} / {target_name}: {reason}", "error")
|
|
return redirect("/ops/send-console")
|
|
|
|
@app.post("/config")
|
|
async def save_runtime_config(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return Response("Invalid CSRF token", status_code=403)
|
|
|
|
config = get_config(force_reload=True)
|
|
if "messageTemplate" in form:
|
|
config["messageTemplate"] = str(form.get("messageTemplate", config.get("messageTemplate", "")))
|
|
if "multiTask" in form:
|
|
config["multiTask"] = str(form.get("multiTask", "")) == "on"
|
|
if "taskCount" in form:
|
|
config["taskCount"] = coerce_int(form.get("taskCount", config.get("taskCount", 1)), config.get("taskCount", 1), 1)
|
|
if "hitokotoTypes" in form:
|
|
raw_types = str(form.get("hitokotoTypes", ""))
|
|
config["hitokotoTypes"] = [item.strip() for item in raw_types.replace(",", "\n").splitlines() if item.strip()]
|
|
|
|
send_strategy = config.get("sendStrategy", {}) or {}
|
|
if "shuffleTargets" in form:
|
|
send_strategy["shuffleTargets"] = str(form.get("shuffleTargets", "")) == "on"
|
|
if "accountStartDelaySecondsMin" in form:
|
|
send_strategy["accountStartDelaySecondsMin"] = coerce_int(
|
|
form.get("accountStartDelaySecondsMin", send_strategy.get("accountStartDelaySecondsMin", 0)),
|
|
send_strategy.get("accountStartDelaySecondsMin", 0),
|
|
0,
|
|
)
|
|
if "accountStartDelaySecondsMax" in form:
|
|
send_strategy["accountStartDelaySecondsMax"] = coerce_int(
|
|
form.get("accountStartDelaySecondsMax", send_strategy.get("accountStartDelaySecondsMax", 0)),
|
|
send_strategy.get("accountStartDelaySecondsMax", 0),
|
|
send_strategy.get("accountStartDelaySecondsMin", 0),
|
|
)
|
|
if "messageIntervalSecondsMin" in form:
|
|
send_strategy["messageIntervalSecondsMin"] = coerce_int(
|
|
form.get("messageIntervalSecondsMin", send_strategy.get("messageIntervalSecondsMin", 0)),
|
|
send_strategy.get("messageIntervalSecondsMin", 0),
|
|
0,
|
|
)
|
|
if "messageIntervalSecondsMax" in form:
|
|
send_strategy["messageIntervalSecondsMax"] = coerce_int(
|
|
form.get("messageIntervalSecondsMax", send_strategy.get("messageIntervalSecondsMax", 0)),
|
|
send_strategy.get("messageIntervalSecondsMax", 0),
|
|
send_strategy.get("messageIntervalSecondsMin", 0),
|
|
)
|
|
if "messageVariants" in form:
|
|
raw_variants = str(form.get("messageVariants", ""))
|
|
send_strategy["messageVariants"] = [
|
|
item.strip() for item in raw_variants.replace("\r", "\n").split("\n") if item.strip()
|
|
]
|
|
config["sendStrategy"] = send_strategy
|
|
|
|
happy_new_year = config.get("happyNewYear", {})
|
|
if "happyNewYearEnabled" in form:
|
|
happy_new_year["enabled"] = str(form.get("happyNewYearEnabled", "")) == "on"
|
|
if "happyNewYearTemplate" in form:
|
|
happy_new_year["messageTemplate"] = str(form.get("happyNewYearTemplate", happy_new_year.get("messageTemplate", "")))
|
|
config["happyNewYear"] = happy_new_year
|
|
save_config(config)
|
|
|
|
flash(request, "Runtime config saved.", "success")
|
|
return redirect("/")
|
|
|
|
@app.post("/settings")
|
|
async def save_panel_settings(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return Response("Invalid CSRF token", status_code=403)
|
|
|
|
settings = get_app_settings(force_reload=True)
|
|
settings["server_host"] = str(form.get("server_host", "")).strip()
|
|
settings["server_username"] = str(form.get("server_username", "")).strip()
|
|
settings["server_password"] = str(form.get("server_password", "")).strip()
|
|
settings["compose_root"] = str(form.get("compose_root", settings.get("compose_root", ""))).strip()
|
|
settings["ops_log_file"] = str(form.get("ops_log_file", settings.get("ops_log_file", ""))).strip()
|
|
settings["proxy_refresh_script"] = str(form.get("proxy_refresh_script", settings.get("proxy_refresh_script", ""))).strip()
|
|
settings["local_login_helper_url"] = str(
|
|
form.get("local_login_helper_url", settings.get("local_login_helper_url", "http://127.0.0.1:18765"))
|
|
).strip()
|
|
settings["login_desktop_api_url"] = str(
|
|
form.get("login_desktop_api_url", settings.get("login_desktop_api_url", "http://127.0.0.1:18090"))
|
|
).strip()
|
|
settings["ui_port"] = int(form.get("ui_port", settings.get("ui_port", 8787)))
|
|
save_app_settings(settings)
|
|
|
|
new_password = str(form.get("new_password", ""))
|
|
confirm_password = str(form.get("confirm_password", ""))
|
|
if new_password:
|
|
if new_password != confirm_password:
|
|
flash(request, "Admin password was not updated because the confirmation did not match.", "error")
|
|
return redirect("/")
|
|
update_admin_password(new_password)
|
|
|
|
flash(request, "Panel settings saved.", "success")
|
|
return redirect("/")
|
|
|
|
@app.post("/ops/run-now")
|
|
async def run_now(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return Response("Invalid CSRF token", status_code=403)
|
|
|
|
pid = run_task_now()
|
|
if pid == -1:
|
|
flash(request, "Failed to start the full resend run. Check server logs for details.", "error")
|
|
else:
|
|
flash(request, f"Triggered a full resend run in the background (pid {pid}).", "success")
|
|
return redirect("/")
|
|
|
|
@app.post("/ops/run-unsent")
|
|
async def run_unsent(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return Response("Invalid CSRF token", status_code=403)
|
|
|
|
pid = run_unsent_retry_now()
|
|
if pid == -1:
|
|
flash(request, "Failed to start the unsent-target fallback run. Check server logs for details.", "error")
|
|
else:
|
|
flash(request, f"Triggered an unsent-target fallback run in the background (pid {pid}).", "success")
|
|
return redirect("/ops/send-console")
|
|
|
|
@app.post("/ops/proxy/refresh")
|
|
async def proxy_refresh(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return Response("Invalid CSRF token", status_code=403)
|
|
|
|
refresh_proxy()
|
|
flash(request, "Proxy subscription refreshed.", "success")
|
|
return redirect("/")
|
|
|
|
@app.post("/ops/proxy/restart")
|
|
async def proxy_restart(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return Response("Invalid CSRF token", status_code=403)
|
|
|
|
restart_proxy()
|
|
flash(request, "Proxy container restarted.", "success")
|
|
return redirect("/")
|
|
|
|
@app.post("/ops/schedule")
|
|
async def save_schedule(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return Response("Invalid CSRF token", status_code=403)
|
|
|
|
time_string = str(form.get("daily_schedule", "")).strip()
|
|
result = update_daily_schedule(time_string)
|
|
if getattr(result, "returncode", 1) == 0:
|
|
flash(request, f"Updated the daily schedule to {time_string}.", "success")
|
|
else:
|
|
flash(request, f"Failed to update the daily schedule to {time_string}: {getattr(result, 'stderr', '')}", "error")
|
|
return redirect("/")
|
|
|
|
@app.get("/ops/logs", response_class=HTMLResponse)
|
|
async def logs_page(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return maybe_redirect
|
|
return render_template(
|
|
request,
|
|
"logs.html",
|
|
{
|
|
"flash": pop_flash(request),
|
|
"log_tail": read_log_tail(400),
|
|
},
|
|
)
|
|
|
|
@app.get("/login-desktop/status")
|
|
async def login_desktop_status(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return JSONResponse({"redirect": "/login"}, status_code=401)
|
|
try:
|
|
payload = call_login_desktop("/status")
|
|
payload["public_url"] = login_desktop_public_url(request)
|
|
return JSONResponse(payload)
|
|
except RuntimeError as exc:
|
|
return JSONResponse({"ok": False, "error": str(exc), "public_url": login_desktop_public_url(request)}, status_code=503)
|
|
|
|
@app.post("/login-desktop/open")
|
|
async def login_desktop_open(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return JSONResponse({"redirect": "/login"}, status_code=401)
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return JSONResponse({"ok": False, "error": "Invalid CSRF token"}, status_code=403)
|
|
try:
|
|
call_login_desktop("/open-login", method="POST", payload={})
|
|
return JSONResponse({"ok": True, "public_url": login_desktop_public_url(request)})
|
|
except RuntimeError as exc:
|
|
return JSONResponse({"ok": False, "error": str(exc)}, status_code=503)
|
|
|
|
@app.post("/login-desktop/reset")
|
|
async def login_desktop_reset(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return JSONResponse({"redirect": "/login"}, status_code=401)
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return JSONResponse({"ok": False, "error": "Invalid CSRF token"}, status_code=403)
|
|
try:
|
|
payload = call_login_desktop("/reset", method="POST", payload={}, timeout=120)
|
|
return JSONResponse({"ok": True, "result": payload})
|
|
except RuntimeError as exc:
|
|
return JSONResponse({"ok": False, "error": str(exc)}, status_code=503)
|
|
|
|
@app.post("/login-desktop/save")
|
|
async def login_desktop_save(request: Request):
|
|
maybe_redirect = require_user(request)
|
|
if maybe_redirect:
|
|
return JSONResponse({"redirect": "/login"}, status_code=401)
|
|
form = await request.form()
|
|
if not validate_csrf(request, str(form.get("csrf_token", ""))):
|
|
return JSONResponse({"ok": False, "error": "Invalid CSRF token"}, status_code=403)
|
|
|
|
relogin_unique_id = str(form.get("relogin_unique_id", "")).strip()
|
|
display_name = str(form.get("display_name", "")).strip()
|
|
try:
|
|
payload = call_login_desktop("/export", method="POST", payload={}, timeout=30)
|
|
if not payload.get("ok"):
|
|
raise RuntimeError("login-desktop export did not return ok")
|
|
account, action = save_exported_login_result(
|
|
payload.get("result", {}),
|
|
relogin_unique_id=relogin_unique_id,
|
|
display_name=display_name,
|
|
)
|
|
return JSONResponse({
|
|
"ok": True,
|
|
"action": action,
|
|
"account": {
|
|
"unique_id": account.get("unique_id"),
|
|
"username": account.get("username"),
|
|
"enabled": account.get("enabled", True),
|
|
},
|
|
})
|
|
except RuntimeError as exc:
|
|
return JSONResponse({"ok": False, "error": str(exc)}, status_code=400)
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|
|
|
|
|
|
def run_web_app(host=None, port=None):
|
|
settings = get_app_settings(force_reload=True)
|
|
uvicorn.run(
|
|
"webui.app:app",
|
|
host=host or settings["ui_host"],
|
|
port=port or settings["ui_port"],
|
|
reload=False,
|
|
)
|