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