1import logging
2from contextlib import asynccontextmanager
3from typing import AsyncGenerator
4
5from apscheduler.schedulers.asyncio import AsyncIOScheduler
6from fastapi import FastAPI, Request, status
7from fastapi.encoders import jsonable_encoder
8from fastapi.exceptions import RequestValidationError
9from fastapi.middleware.cors import CORSMiddleware
10from fastapi.responses import HTMLResponse, JSONResponse
11from fastapi_pagination import add_pagination
12from starlette.staticfiles import StaticFiles
13from uvicorn import Config, Server
14
15from app.config.env import (
16 DEBUG,
17 DOCS,
18 HOME_PAGE_TEMPLATE,
19 UVICORN_HOST,
20 UVICORN_PORT,
21 UVICORN_SSL_CERTFILE,
22 UVICORN_SSL_KEYFILE,
23 UVICORN_UDS,
24 DASHBOARD_PATH,
25 TASKS_RECORD_USER_USAGES_INTERVAL,
26 TASKS_REVIEW_USERS_INTERVAL,
27 TASKS_EXPIRE_DAYS_REACHED_INTERVAL,
28 TASKS_RESET_USER_DATA_USAGE,
29)
30from app.templates import render_template
31from . import __version__
32from .routes import api_router
33from .tasks import (
34 nodes_startup,
35 record_user_usages,
36 reset_user_data_usage,
37 review_users,
38 expire_days_reached,
39)
40from .webhooks import webhooks_router
41
42logger = logging.getLogger(__name__)
43
44
45@asynccontextmanager
46async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
47 await nodes_startup()
48 yield
49 scheduler.shutdown()
50
51
52app = FastAPI(
53 title="MarzneshinAPI",
54 description="Unified GUI Censorship Resistant Solution Powered by Xray",
55 version=__version__,
56 lifespan=lifespan,
57 docs_url="/docs" if DOCS else None,
58 redoc_url="/redoc" if DOCS else None,
59)
60
61app.webhooks.include_router(webhooks_router)
62
63app.include_router(api_router)
64add_pagination(app)
65
66
67@app.get("/", response_class=HTMLResponse)
68def home_page():
69 return render_template(HOME_PAGE_TEMPLATE)
70
71
72app.add_middleware(
73 CORSMiddleware,
74 allow_origins=["*"],
75 allow_credentials=True,
76 allow_methods=["*"],
77 allow_headers=["*"],
78)
79
80scheduler = AsyncIOScheduler(timezone="UTC")
81scheduler.add_job(
82 record_user_usages,
83 "interval",
84 coalesce=True,
85 seconds=TASKS_RECORD_USER_USAGES_INTERVAL,
86)
87scheduler.add_job(
88 review_users,
89 "interval",
90 seconds=TASKS_REVIEW_USERS_INTERVAL,
91 coalesce=True,
92 max_instances=1,
93)
94scheduler.add_job(
95 expire_days_reached,
96 "interval",
97 seconds=TASKS_EXPIRE_DAYS_REACHED_INTERVAL,
98 coalesce=True,
99 max_instances=1,
100)
101scheduler.add_job(
102 reset_user_data_usage,
103 "interval",
104 seconds=TASKS_RESET_USER_DATA_USAGE,
105 coalesce=True,
106)
107
108
109@app.exception_handler(RequestValidationError)
110def validation_exception_handler(
111 request: Request, exc: RequestValidationError
112):
113 details = {}
114 for error in exc.errors():
115 details[error["loc"][-1]] = error.get("msg")
116 return JSONResponse(
117 status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
118 content=jsonable_encoder({"detail": details}),
119 )
120
121
122async def main():
123 if not DEBUG:
124 app.mount(
125 DASHBOARD_PATH,
126 StaticFiles(directory="dashboard/dist", html=True),
127 name="dashboard",
128 )
129 app.mount(
130 "/static/",
131 StaticFiles(directory="dashboard/dist/static"),
132 name="static",
133 )
134 app.mount(
135 "/locales/",
136 StaticFiles(directory="dashboard/dist/locales"),
137 name="locales",
138 )
139 scheduler.start()
140 cfg = Config(
141 app=app,
142 host=UVICORN_HOST,
143 port=UVICORN_PORT,
144 uds=(None if DEBUG else UVICORN_UDS),
145 ssl_certfile=UVICORN_SSL_CERTFILE,
146 ssl_keyfile=UVICORN_SSL_KEYFILE,
147 workers=1,
148 reload=DEBUG,
149 log_level=logging.DEBUG if DEBUG else logging.INFO,
150 )
151 server = Server(cfg)
152 await server.serve()