This commit is contained in:
Manchik
2026-04-17 22:22:40 +03:00
commit c82866b58a
135 changed files with 18493 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
import mimetypes
import socket
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
mimetypes.add_type("application/javascript", ".js")
mimetypes.add_type("text/css", ".css")
mimetypes.add_type("image/svg+xml", ".svg")
from loguru import logger
VERSION = "0.3.2"
from app.config import settings
from app.core.logging import setup_logging
from app.database import get_db, close_db
from app.core.proxy_manager import proxy_manager
from app.core.auto_accept import auto_accept_manager
from app.services.registry import register_all_handlers
from app.api import accounts, actions, proxies, tasks, mafile_tools, logs, auto_accept, browser
from app.api import settings as settings_api
from app.api import logpass, token_accounts
@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)
yield
await auto_accept_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(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,
)