1import asyncio
2import logging
3from contextlib import asynccontextmanager
4from datetime import datetime as dt
5from datetime import timedelta as td
6from typing import AsyncGenerator
7
8from apscheduler.schedulers.asyncio import AsyncIOScheduler
9from fastapi import FastAPI, Request, status
10from fastapi.encoders import jsonable_encoder
11from fastapi.exceptions import RequestValidationError
12from fastapi.middleware.cors import CORSMiddleware
13from fastapi.responses import HTMLResponse, JSONResponse
14from fastapi_pagination import add_pagination
15from starlette.staticfiles import StaticFiles
16from uvicorn import Config, Server
17
18from app.config.env 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 WEBHOOK_ADDRESS,
29)
30from app.templates import render_template
31from . import __version__, telegram
32from .routes import api_router
33from .tasks import (
34 delete_expired_reminders,
35 nodes_startup,
36 record_user_usages,
37 reset_user_data_usage,
38 review_users,
39 send_notifications,
40)
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 logger.info("Sending pending notifications before shutdown...")
51 await send_notifications()
52
53
54app = FastAPI(
55 title="MarzneshinAPI",
56 description="Unified GUI Censorship Resistant Solution Powered by Xray",
57 version=__version__,
58 lifespan=lifespan,
59 docs_url="/docs" if DOCS else None,
60 redoc_url="/redoc" if DOCS else None,
61)
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(record_user_usages, "interval", coalesce=True, seconds=30)
82scheduler.add_job(
83 review_users, "interval", seconds=30, coalesce=True, max_instances=1
84)
85scheduler.add_job(reset_user_data_usage, "interval", coalesce=True, hours=1)
86
87if WEBHOOK_ADDRESS:
88 scheduler.add_job(
89 send_notifications, "interval", seconds=30, replace_existing=True
90 )
91 scheduler.add_job(
92 delete_expired_reminders,
93 "interval",
94 hours=2,
95 start_date=dt.utcnow() + td(minutes=1),
96 )
97
98
99@app.exception_handler(RequestValidationError)
100def validation_exception_handler(
101 request: Request, exc: RequestValidationError
102):
103 details = {}
104 for error in exc.errors():
105 details[error["loc"][-1]] = error.get("msg")
106 return JSONResponse(
107 status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
108 content=jsonable_encoder({"detail": details}),
109 )
110
111
112async def main():
113 if not DEBUG:
114 app.mount(
115 DASHBOARD_PATH,
116 StaticFiles(directory="dashboard/dist", html=True),
117 name="dashboard",
118 )
119 app.mount(
120 "/static/",
121 StaticFiles(directory="dashboard/dist/static"),
122 name="static",
123 )
124 app.mount(
125 "/locales/",
126 StaticFiles(directory="dashboard/dist/locales"),
127 name="locales",
128 )
129 scheduler.start()
130 asyncio.create_task(telegram.start_bot())
131 cfg = Config(
132 app=app,
133 host=UVICORN_HOST,
134 port=UVICORN_PORT,
135 uds=(None if DEBUG else UVICORN_UDS),
136 ssl_certfile=UVICORN_SSL_CERTFILE,
137 ssl_keyfile=UVICORN_SSL_KEYFILE,
138 workers=1,
139 reload=DEBUG,
140 log_level=logging.DEBUG if DEBUG else logging.INFO,
141 )
142 server = Server(cfg)
143 await server.serve()