aa
This commit is contained in:
193
app_web.py
193
app_web.py
@@ -7,6 +7,7 @@ import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import sqlite3
|
||||
import time
|
||||
import traceback
|
||||
@@ -16,8 +17,9 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from fastapi import FastAPI, Form, Query, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
ENV_FILE = BASE_DIR / ".env"
|
||||
@@ -30,7 +32,43 @@ os.environ.setdefault("TELEGRAM_WEB_UI", "1")
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
|
||||
WEB_CONSOLE_AUTH_KEY = "web_console_authenticated"
|
||||
|
||||
|
||||
def _web_console_password() -> str:
|
||||
return os.getenv("WEB_CONSOLE_PASSWORD", "Aa123456")
|
||||
|
||||
|
||||
def _web_session_secret() -> str:
|
||||
s = (os.getenv("WEB_CONSOLE_SESSION_SECRET") or "").strip()
|
||||
if s:
|
||||
return s
|
||||
return "telegram-scraper-web-console-dev-secret-change-me"
|
||||
|
||||
|
||||
def is_console_authed(request: Request) -> bool:
|
||||
return bool(request.session.get(WEB_CONSOLE_AUTH_KEY))
|
||||
|
||||
|
||||
def redirect_if_console_unauthed(request: Request) -> Optional[RedirectResponse]:
|
||||
if is_console_authed(request):
|
||||
return None
|
||||
return RedirectResponse(url="/?needauth=1", status_code=303)
|
||||
|
||||
|
||||
def json_if_console_unauthed(request: Request) -> Optional[JSONResponse]:
|
||||
if is_console_authed(request):
|
||||
return None
|
||||
return JSONResponse({"ok": False, "error": "需要控制台密码验证"}, status_code=401)
|
||||
|
||||
|
||||
app = FastAPI(title="Telegram Scraper Web Console")
|
||||
app.add_middleware(
|
||||
SessionMiddleware,
|
||||
secret_key=_web_session_secret(),
|
||||
max_age=14 * 24 * 3600,
|
||||
same_site="lax",
|
||||
)
|
||||
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||
|
||||
# Starlette 较新版本:TemplateResponse(request, name, context);旧版:(name, context)
|
||||
@@ -608,6 +646,41 @@ def compute_storage_stats(base: Path, days: int, keyword_list: Tuple[str, ...])
|
||||
}
|
||||
|
||||
|
||||
@app.get("/auth/console/status")
|
||||
async def auth_console_status(request: Request):
|
||||
return {"ok": is_console_authed(request)}
|
||||
|
||||
|
||||
@app.post("/auth/console/login")
|
||||
async def auth_console_login(request: Request):
|
||||
ct = (request.headers.get("content-type") or "").lower()
|
||||
password = ""
|
||||
if "application/json" in ct:
|
||||
try:
|
||||
body = await request.json()
|
||||
password = str(body.get("password", ""))
|
||||
except Exception:
|
||||
password = ""
|
||||
else:
|
||||
form = await request.form()
|
||||
password = str(form.get("password", ""))
|
||||
expected = _web_console_password()
|
||||
try:
|
||||
ok = secrets.compare_digest(password.encode("utf-8"), expected.encode("utf-8"))
|
||||
except Exception:
|
||||
ok = False
|
||||
if ok:
|
||||
request.session[WEB_CONSOLE_AUTH_KEY] = True
|
||||
return {"ok": True}
|
||||
return JSONResponse({"ok": False, "error": "密码错误"}, status_code=401)
|
||||
|
||||
|
||||
@app.post("/auth/console/logout")
|
||||
async def auth_console_logout(request: Request):
|
||||
request.session.pop(WEB_CONSOLE_AUTH_KEY, None)
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
|
||||
|
||||
@app.get("/api/stats/overview")
|
||||
async def api_stats_overview(
|
||||
days: int = Query(30, ge=1, le=366),
|
||||
@@ -636,10 +709,13 @@ async def index(request: Request):
|
||||
monitored_ids = {str(row["channel_id"]) for row in monitored}
|
||||
account_channels: List[Dict] = []
|
||||
err = ""
|
||||
try:
|
||||
account_channels = await service.list_account_channels()
|
||||
except Exception as e:
|
||||
err = str(e)
|
||||
authed = is_console_authed(request)
|
||||
if authed:
|
||||
try:
|
||||
account_channels = await service.list_account_channels()
|
||||
except Exception as e:
|
||||
err = str(e)
|
||||
logs_text = "\n".join(service.logs) if authed else ""
|
||||
return template_response(
|
||||
request,
|
||||
"index.html",
|
||||
@@ -649,13 +725,15 @@ async def index(request: Request):
|
||||
"monitored": monitored,
|
||||
"monitored_ids": monitored_ids,
|
||||
"account_channels": account_channels,
|
||||
"console_authed": authed,
|
||||
"need_auth_banner": request.query_params.get("needauth") == "1",
|
||||
"connected": service.is_connected(),
|
||||
"job_running": service.is_job_running(),
|
||||
"job_name": service.job_name,
|
||||
"job_uptime": service.current_job_uptime(),
|
||||
"continuous_running": service.is_continuous_running(),
|
||||
"continuous_uptime": service.continuous_uptime(),
|
||||
"logs": "\n".join(service.logs),
|
||||
"logs": logs_text,
|
||||
"error": err,
|
||||
},
|
||||
)
|
||||
@@ -684,6 +762,9 @@ async def index(request: Request):
|
||||
|
||||
@app.post("/config")
|
||||
async def save_config(request: Request):
|
||||
redir = redirect_if_console_unauthed(request)
|
||||
if redir:
|
||||
return redir
|
||||
form = await request.form()
|
||||
updates: Dict[str, str] = {}
|
||||
for k, _label, _field_type in CONFIG_KEYS:
|
||||
@@ -696,7 +777,10 @@ async def save_config(request: Request):
|
||||
|
||||
|
||||
@app.post("/start")
|
||||
async def start_scraper():
|
||||
async def start_scraper(request: Request):
|
||||
redir = redirect_if_console_unauthed(request)
|
||||
if redir:
|
||||
return redir
|
||||
try:
|
||||
await service.ensure_ready()
|
||||
except Exception as e:
|
||||
@@ -705,13 +789,19 @@ async def start_scraper():
|
||||
|
||||
|
||||
@app.post("/stop")
|
||||
async def stop_scraper():
|
||||
async def stop_scraper(request: Request):
|
||||
redir = redirect_if_console_unauthed(request)
|
||||
if redir:
|
||||
return redir
|
||||
await service.disconnect()
|
||||
return RedirectResponse(url="/", status_code=303)
|
||||
|
||||
|
||||
@app.post("/channels/add")
|
||||
async def add_channel(channel_spec: str = Form(...)):
|
||||
async def add_channel(request: Request, channel_spec: str = Form(...)):
|
||||
redir = redirect_if_console_unauthed(request)
|
||||
if redir:
|
||||
return redir
|
||||
try:
|
||||
msg = await service.add_channel(channel_spec)
|
||||
service._append(msg)
|
||||
@@ -722,6 +812,9 @@ async def add_channel(channel_spec: str = Form(...)):
|
||||
|
||||
@app.post("/channels/add-selected")
|
||||
async def add_channels_selected(request: Request):
|
||||
redir = redirect_if_console_unauthed(request)
|
||||
if redir:
|
||||
return redir
|
||||
form = await request.form()
|
||||
raw_ids = form.getlist("channel_id")
|
||||
ids = [str(x).strip() for x in raw_ids if str(x).strip()]
|
||||
@@ -738,7 +831,10 @@ async def add_channels_selected(request: Request):
|
||||
|
||||
|
||||
@app.post("/channels/remove")
|
||||
async def remove_channel(channel_spec: str = Form(...)):
|
||||
async def remove_channel(request: Request, channel_spec: str = Form(...)):
|
||||
redir = redirect_if_console_unauthed(request)
|
||||
if redir:
|
||||
return redir
|
||||
try:
|
||||
msg = await service.remove_channel(channel_spec)
|
||||
service._append(msg)
|
||||
@@ -748,7 +844,10 @@ async def remove_channel(channel_spec: str = Form(...)):
|
||||
|
||||
|
||||
@app.post("/jobs/scrape")
|
||||
async def start_scrape(selection: str = Form("all")):
|
||||
async def start_scrape(request: Request, selection: str = Form("all")):
|
||||
redir = redirect_if_console_unauthed(request)
|
||||
if redir:
|
||||
return redir
|
||||
try:
|
||||
msg = await service.start_scrape_job(selection)
|
||||
service._append(msg)
|
||||
@@ -758,7 +857,10 @@ async def start_scrape(selection: str = Form("all")):
|
||||
|
||||
|
||||
@app.post("/jobs/export")
|
||||
async def start_export(selection: str = Form("all")):
|
||||
async def start_export(request: Request, selection: str = Form("all")):
|
||||
redir = redirect_if_console_unauthed(request)
|
||||
if redir:
|
||||
return redir
|
||||
try:
|
||||
msg = await service.start_export_job(selection)
|
||||
service._append(msg)
|
||||
@@ -768,7 +870,10 @@ async def start_export(selection: str = Form("all")):
|
||||
|
||||
|
||||
@app.post("/jobs/rescrape")
|
||||
async def start_rescrape(selection: str = Form("all")):
|
||||
async def start_rescrape(request: Request, selection: str = Form("all")):
|
||||
redir = redirect_if_console_unauthed(request)
|
||||
if redir:
|
||||
return redir
|
||||
try:
|
||||
msg = await service.start_rescrape_job(selection)
|
||||
service._append(msg)
|
||||
@@ -778,7 +883,10 @@ async def start_rescrape(selection: str = Form("all")):
|
||||
|
||||
|
||||
@app.post("/jobs/continuous/start")
|
||||
async def start_continuous():
|
||||
async def start_continuous(request: Request):
|
||||
redir = redirect_if_console_unauthed(request)
|
||||
if redir:
|
||||
return redir
|
||||
try:
|
||||
msg = await service.start_continuous()
|
||||
service._append(msg)
|
||||
@@ -788,7 +896,10 @@ async def start_continuous():
|
||||
|
||||
|
||||
@app.post("/jobs/continuous/stop")
|
||||
async def stop_continuous():
|
||||
async def stop_continuous(request: Request):
|
||||
redir = redirect_if_console_unauthed(request)
|
||||
if redir:
|
||||
return redir
|
||||
try:
|
||||
msg = await service.stop_continuous()
|
||||
service._append(msg)
|
||||
@@ -798,7 +909,10 @@ async def stop_continuous():
|
||||
|
||||
|
||||
@app.get("/status")
|
||||
async def status():
|
||||
async def status(request: Request):
|
||||
denied = json_if_console_unauthed(request)
|
||||
if denied:
|
||||
return denied
|
||||
return {
|
||||
"ready": service.scraper is not None,
|
||||
"connected": service.is_connected(),
|
||||
@@ -818,7 +932,10 @@ async def api_monitored_channels():
|
||||
|
||||
|
||||
@app.get("/api/channels/account")
|
||||
async def api_account_channels():
|
||||
async def api_account_channels(request: Request):
|
||||
denied = json_if_console_unauthed(request)
|
||||
if denied:
|
||||
return denied
|
||||
try:
|
||||
items = await service.list_account_channels()
|
||||
return {"items": items}
|
||||
@@ -827,62 +944,84 @@ async def api_account_channels():
|
||||
|
||||
|
||||
@app.post("/api/channels/add")
|
||||
async def api_add_channel(channel_spec: str = Form(...)):
|
||||
async def api_add_channel(request: Request, channel_spec: str = Form(...)):
|
||||
denied = json_if_console_unauthed(request)
|
||||
if denied:
|
||||
return denied
|
||||
msg = await service.add_channel(channel_spec)
|
||||
service._append(msg)
|
||||
return {"ok": True, "message": msg}
|
||||
|
||||
|
||||
@app.post("/api/channels/remove")
|
||||
async def api_remove_channel(channel_spec: str = Form(...)):
|
||||
async def api_remove_channel(request: Request, channel_spec: str = Form(...)):
|
||||
denied = json_if_console_unauthed(request)
|
||||
if denied:
|
||||
return denied
|
||||
msg = await service.remove_channel(channel_spec)
|
||||
service._append(msg)
|
||||
return {"ok": True, "message": msg}
|
||||
|
||||
|
||||
@app.post("/api/jobs/scrape")
|
||||
async def api_job_scrape(selection: str = Form("all")):
|
||||
async def api_job_scrape(request: Request, selection: str = Form("all")):
|
||||
denied = json_if_console_unauthed(request)
|
||||
if denied:
|
||||
return denied
|
||||
msg = await service.start_scrape_job(selection)
|
||||
service._append(msg)
|
||||
return {"ok": True, "message": msg}
|
||||
|
||||
|
||||
@app.post("/api/jobs/export")
|
||||
async def api_job_export(selection: str = Form("all")):
|
||||
async def api_job_export(request: Request, selection: str = Form("all")):
|
||||
denied = json_if_console_unauthed(request)
|
||||
if denied:
|
||||
return denied
|
||||
msg = await service.start_export_job(selection)
|
||||
service._append(msg)
|
||||
return {"ok": True, "message": msg}
|
||||
|
||||
|
||||
@app.post("/api/jobs/rescrape")
|
||||
async def api_job_rescrape(selection: str = Form("all")):
|
||||
async def api_job_rescrape(request: Request, selection: str = Form("all")):
|
||||
denied = json_if_console_unauthed(request)
|
||||
if denied:
|
||||
return denied
|
||||
msg = await service.start_rescrape_job(selection)
|
||||
service._append(msg)
|
||||
return {"ok": True, "message": msg}
|
||||
|
||||
|
||||
@app.get("/api/jobs/status")
|
||||
async def api_job_status():
|
||||
return {
|
||||
async def api_job_status(request: Request):
|
||||
payload = {
|
||||
"connected": service.is_connected(),
|
||||
"job_running": service.is_job_running(),
|
||||
"job_name": service.job_name,
|
||||
"job_uptime": service.current_job_uptime(),
|
||||
"continuous_running": service.is_continuous_running(),
|
||||
"continuous_uptime": service.continuous_uptime(),
|
||||
"logs": list(service.logs),
|
||||
"logs": list(service.logs) if is_console_authed(request) else [],
|
||||
}
|
||||
return payload
|
||||
|
||||
|
||||
@app.post("/api/jobs/continuous/start")
|
||||
async def api_start_continuous():
|
||||
async def api_start_continuous(request: Request):
|
||||
denied = json_if_console_unauthed(request)
|
||||
if denied:
|
||||
return denied
|
||||
msg = await service.start_continuous()
|
||||
service._append(msg)
|
||||
return {"ok": True, "message": msg}
|
||||
|
||||
|
||||
@app.post("/api/jobs/continuous/stop")
|
||||
async def api_stop_continuous():
|
||||
async def api_stop_continuous(request: Request):
|
||||
denied = json_if_console_unauthed(request)
|
||||
if denied:
|
||||
return denied
|
||||
msg = await service.stop_continuous()
|
||||
service._append(msg)
|
||||
return {"ok": True, "message": msg}
|
||||
|
||||
Reference in New Issue
Block a user