Files
Steam-Panel/main.py
T
Manchik 68433b74c0 v3.1.0
2026-05-19 18:28:43 +03:00

203 lines
6.1 KiB
Python
Executable File
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 mimetypes
import socket
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from loguru import logger
from app.api import (
accounts,
actions,
auto_accept,
auto_confirm,
browser,
logpass,
logs,
mafile_tools,
proxies,
tasks,
token_accounts,
)
from app.api import settings as settings_api
from app.config import settings
from app.core.auto_accept import auto_accept_manager
from app.core.auto_confirm import auto_confirm_manager
from app.core.logging import setup_logging
from app.core.proxy_manager import proxy_manager
from app.database import close_db, get_db
from app.services.registry import register_all_handlers
from version import VERSION
mimetypes.add_type("application/javascript", ".js")
mimetypes.add_type("text/css", ".css")
mimetypes.add_type("image/svg+xml", ".svg")
@asynccontextmanager
async def lifespan(app: FastAPI):
setup_logging()
from app.api.logs import install_log_sink
install_log_sink()
db = await get_db()
# Clean up stale tasks left from a previous unclean shutdown
stale = await db.execute(
"UPDATE tasks SET status = 'cancelled', error = 'Server restarted', updated_at = datetime('now') "
"WHERE status = 'running'"
)
if stale.rowcount:
logger.info(f"Startup cleanup: cancelled {stale.rowcount} stale task(s)")
# Clear per-account result icons so they don't persist after restart
await db.execute(
"UPDATE tasks SET account_results = NULL WHERE account_results IS NOT NULL"
)
await db.commit()
await proxy_manager.load()
register_all_handlers()
# Resume auto-accept for accounts that had it enabled
cursor = await db.execute("SELECT * FROM accounts WHERE auto_accept = 1")
rows = await cursor.fetchall()
for row in rows:
account = dict(row)
if account.get("shared_secret"):
await auto_accept_manager.start(account)
# Resume auto-confirm for accounts that had it enabled
cursor = await db.execute("SELECT * FROM accounts WHERE auto_confirm = 1")
rows = await cursor.fetchall()
for row in rows:
account = dict(row)
if account.get("identity_secret"):
await auto_confirm_manager.start(account)
yield
await auto_accept_manager.stop_all()
await auto_confirm_manager.stop_all()
await close_db()
app = FastAPI(
title=settings.app_name,
version=VERSION,
docs_url="/docs" if settings.debug else None,
lifespan=lifespan,
)
@app.get("/api/version")
async def get_version():
return {"version": VERSION}
app.include_router(accounts.router)
app.include_router(actions.router)
app.include_router(proxies.router)
app.include_router(tasks.router)
app.include_router(mafile_tools.router)
app.include_router(logs.router)
app.include_router(auto_accept.router)
app.include_router(auto_confirm.router)
app.include_router(browser.router)
app.include_router(settings_api.router)
app.include_router(logpass.router)
app.include_router(token_accounts.router)
static_dir = settings.base_dir / "app" / "static"
static_dir.mkdir(parents=True, exist_ok=True)
_MIME = {".js": "application/javascript", ".css": "text/css", ".svg": "image/svg+xml"}
@app.get("/", include_in_schema=False)
async def serve_root():
return FileResponse(
str(static_dir / "index.html"),
headers={"Cache-Control": "no-store, no-cache, must-revalidate"},
)
@app.get("/assets/{filename:path}", include_in_schema=False)
async def serve_asset(filename: str):
from fastapi import HTTPException
file_path = static_dir / "assets" / filename
if not file_path.exists():
raise HTTPException(status_code=404)
ext = "." + filename.rsplit(".", 1)[-1] if "." in filename else ""
media_type = _MIME.get(ext, "application/octet-stream")
return FileResponse(
str(file_path),
media_type=media_type,
headers={"Cache-Control": "public, max-age=31536000, immutable"},
)
app.mount("/", StaticFiles(directory=str(static_dir), html=True), name="static")
def find_free_port(start: int = 8000, limit: int = 200) -> int:
"""Return the first free TCP port starting from `start`."""
for port in range(start, start + limit):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
s.bind(("127.0.0.1", port))
return port
except OSError:
continue
raise RuntimeError(f"No free port in range {start}{start + limit - 1}")
if __name__ == "__main__":
import threading
import time
import webbrowser
import uvicorn
# Install log intercept before uvicorn.run() so the first two startup
# lines (Started server process / Waiting for application startup) are
# captured by our formatter instead of printing raw.
setup_logging()
port = find_free_port(settings.port)
url = f"http://{settings.host}:{port}"
tg = "@lolzdm -> https://t.me/lolzdm"
val_w = max(len(url), len(tg))
inner = val_w + 8 # " URL | " (7) + value + " " (1)
banner = (
"\n"
"\033[34m"
" ╔═╗╔╦╗╔═╗╔═╗╔╦╗╔═╗╔═╗╔╗╔╔═╗╦\n"
" ╚═╗ ║ ║╣ ╠═╣║║║╠═╝╠═╣║║║║╣ ║\n"
" ╚═╝ ╩ ╚═╝╩ ╩╩ ╩╩ ╩ ╩╝╚╝╚═╝╩═╝"
f"\033[0m \033[90mv{VERSION}\033[0m\n\n"
f" \033[90m┌{'' * inner}\033[0m\n"
f" \033[90m│\033[0m\033[34m URL \033[90m│\033[0m {url:<{val_w + 1}}\033[90m│\033[0m\n"
f" \033[90m│\033[0m\033[34m TG \033[90m│\033[0m {tg:<{val_w + 1}}\033[90m│\033[0m\n"
f" \033[90m└{'' * inner}\033[0m\n"
)
print(banner)
def _open_browser():
time.sleep(1.5)
webbrowser.open(url)
threading.Thread(target=_open_browser, daemon=True).start()
uvicorn.run(
"main:app",
host=settings.host,
port=port,
reload=settings.debug,
log_config=None,
)