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 DASHBOARD_PATH,
28)
29
30from . import __version__, telegram
31from .routes import api_router
32from .tasks import (
33 delete_expired_reminders,
34 nodes_startup,
35 record_realtime_bandwidth,
36 record_user_usages,
37 reset_user_data_usage,
38 review_users,
39 send_notifications,
40)
41
42logger = logging.getLogger(__name__)
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_PATH,
114 StaticFiles(directory="dashboard/dist", html=True),
115 name="dashboard",
116 )
117 app.mount(
118 "/static/",
119 StaticFiles(directory="dashboard/dist/static"),
120 name="static",
121 )
122 app.mount(
123 "/locales/",
124 StaticFiles(directory="dashboard/dist/locales"),
125 name="locales",
126 )
127 scheduler.start()
128 asyncio.create_task(telegram.start_bot())
129 cfg = Config(
130 app=app,
131 host=UVICORN_HOST,
132 port=UVICORN_PORT,
133 uds=(None if DEBUG else UVICORN_UDS),
134 ssl_certfile=UVICORN_SSL_CERTFILE,
135 ssl_keyfile=UVICORN_SSL_KEYFILE,
136 workers=1,
137 reload=DEBUG,
138 log_level=logging.DEBUG if DEBUG else logging.INFO,
139 )
140 server = Server(cfg)
141 await server.serve()