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