mirror of
https://github.com/andrey271192/Domain_web.git
synced 2026-09-20 14:41:58 +00:00
feat(deploy): full one-line install with search API
Bundle static site, FastAPI search, nginx proxy, and systemd unit so a fresh Ubuntu VPS can run curl install.sh and get /search working. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
139
api/main.py
Normal file
139
api/main.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Domain Web search API — reuses domain-finder-bot discovery."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
|
||||
BOT_DIR = os.environ.get("DOMAIN_FINDER_BOT_DIR", "/opt/domain-finder-bot")
|
||||
if BOT_DIR not in sys.path:
|
||||
sys.path.insert(0, BOT_DIR)
|
||||
|
||||
from discovery import ( # noqa: E402
|
||||
MAX_BATCH_DOMAINS,
|
||||
discover_batch_with_timeout,
|
||||
discover_with_timeout,
|
||||
format_batch_domains_column,
|
||||
format_batch_full_report,
|
||||
format_batch_ips_column,
|
||||
format_batch_v2fly_column,
|
||||
format_domains_column,
|
||||
format_full_report,
|
||||
format_ips_column,
|
||||
format_v2fly_column,
|
||||
parse_domains_from_message,
|
||||
)
|
||||
from router_bat import ( # noqa: E402
|
||||
format_router_bat,
|
||||
format_router_bat_batch,
|
||||
sanitize_bat_filename,
|
||||
sanitize_batch_bat_filename,
|
||||
)
|
||||
from v2fly_lookup import start_background_sync # noqa: E402
|
||||
|
||||
MAX_CONCURRENT = max(1, int(os.environ.get("MAX_CONCURRENT_SEARCHES", "4")))
|
||||
_SEARCH_SEM = asyncio.Semaphore(MAX_CONCURRENT)
|
||||
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
domain: str | None = None
|
||||
domains: list[str] | None = None
|
||||
|
||||
|
||||
class SearchResponse(BaseModel):
|
||||
domains: list[str]
|
||||
is_batch: bool
|
||||
report: str
|
||||
domains_column: str
|
||||
ips_column: str
|
||||
geosite_geoip: str
|
||||
bat_content: str
|
||||
bat_filename: str
|
||||
|
||||
|
||||
def _resolve_domains(body: SearchRequest) -> list[str]:
|
||||
if body.domains:
|
||||
raw = "\n".join(body.domains)
|
||||
elif body.domain:
|
||||
raw = body.domain
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Provide domain or domains")
|
||||
|
||||
domains = parse_domains_from_message(raw)
|
||||
if not domains:
|
||||
raise HTTPException(status_code=400, detail="No valid domains")
|
||||
if len(domains) > MAX_BATCH_DOMAINS:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Max {MAX_BATCH_DOMAINS} domains per request",
|
||||
)
|
||||
return domains
|
||||
|
||||
|
||||
def _build_response(domains: list[str], results: list[Any]) -> SearchResponse:
|
||||
is_batch = len(domains) > 1
|
||||
if is_batch:
|
||||
report = format_batch_full_report(results)
|
||||
domains_column = format_batch_domains_column(results)
|
||||
ips_column = format_batch_ips_column(results)
|
||||
geosite_geoip = format_batch_v2fly_column(results)
|
||||
bat_content = format_router_bat_batch(results)
|
||||
bat_filename = sanitize_batch_bat_filename(domains[0], len(domains))
|
||||
else:
|
||||
result = results[0]
|
||||
domain = domains[0]
|
||||
report = format_full_report(result)
|
||||
domains_column = format_domains_column(result)
|
||||
ips_column = format_ips_column(result)
|
||||
geosite_geoip = format_v2fly_column(result)
|
||||
bat_content = format_router_bat(domain, result)
|
||||
bat_filename = sanitize_bat_filename(domain)
|
||||
|
||||
return SearchResponse(
|
||||
domains=domains,
|
||||
is_batch=is_batch,
|
||||
report=report,
|
||||
domains_column=domains_column,
|
||||
ips_column=ips_column,
|
||||
geosite_geoip=geosite_geoip,
|
||||
bat_content=bat_content,
|
||||
bat_filename=bat_filename,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
start_background_sync()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(title="Domain Web Search API", lifespan=lifespan)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["POST", "GET", "OPTIONS"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/api/search", response_model=SearchResponse)
|
||||
async def search(body: SearchRequest) -> SearchResponse:
|
||||
domains = _resolve_domains(body)
|
||||
async with _SEARCH_SEM:
|
||||
if len(domains) == 1:
|
||||
results = [await discover_with_timeout(domains[0])]
|
||||
else:
|
||||
results = await discover_batch_with_timeout(domains)
|
||||
return _build_response(domains, results)
|
||||
Reference in New Issue
Block a user