137 lines
3.8 KiB
Python
137 lines
3.8 KiB
Python
import asyncio
|
|
from collections.abc import AsyncGenerator
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
import asyncpg
|
|
from dishka.integrations.fastapi import DishkaRoute, FromDishka, setup_dishka
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import FileResponse, RedirectResponse
|
|
from fastmcp.utilities.lifespan import combine_lifespans
|
|
from starlette.applications import Starlette
|
|
|
|
from api.access_log import hide_mcp_query
|
|
from api.auth import BearerAuthMiddleware
|
|
from api.mcp.server import mcp
|
|
from api.realtime import hub
|
|
from api.routers import (
|
|
accounts,
|
|
analytics,
|
|
annotations,
|
|
avatars,
|
|
backfill,
|
|
bots,
|
|
chats,
|
|
custom_emoji,
|
|
discover,
|
|
events,
|
|
files,
|
|
folders,
|
|
media,
|
|
peers,
|
|
policy,
|
|
presence,
|
|
profile,
|
|
search,
|
|
shares,
|
|
social,
|
|
storage,
|
|
stories,
|
|
watches,
|
|
)
|
|
from dependencies.container import container
|
|
from utils.cache import DAY_HEADERS, IMMUTABLE_HEADERS, NO_STORE_HEADERS
|
|
from utils.env import env
|
|
from utils.storage import ContentAddressedStorage
|
|
from utils.trash import purge_loop
|
|
|
|
if env.auth.token is None:
|
|
msg = "AUTH__TOKEN is required for the api process"
|
|
raise RuntimeError(msg)
|
|
_token = env.auth.token.get_secret_value()
|
|
|
|
hide_mcp_query()
|
|
|
|
mcp_app = mcp.http_app(path="/")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app_: Starlette) -> AsyncGenerator[None]:
|
|
pool = await container.get(asyncpg.Pool)
|
|
await hub.start(pool)
|
|
purger = asyncio.create_task(
|
|
purge_loop(pool, await container.get(ContentAddressedStorage))
|
|
)
|
|
yield
|
|
purger.cancel()
|
|
await hub.stop()
|
|
await app_.state.dishka_container.close()
|
|
|
|
|
|
app = FastAPI(
|
|
title="beavergram API", lifespan=combine_lifespans(lifespan, mcp_app.lifespan)
|
|
)
|
|
app.router.route_class = DishkaRoute
|
|
|
|
|
|
@app.get("/health")
|
|
async def health(pool: FromDishka[asyncpg.Pool]) -> dict[str, bool]:
|
|
db_ok = await pool.fetchval("SELECT 1") == 1
|
|
timescale_ok = await pool.fetchval(
|
|
"SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb')"
|
|
)
|
|
return {"db": db_ok, "timescaledb": bool(timescale_ok)}
|
|
|
|
|
|
app.include_router(accounts.router)
|
|
app.include_router(analytics.router)
|
|
app.include_router(bots.router)
|
|
app.include_router(policy.router)
|
|
app.include_router(folders.router)
|
|
app.include_router(backfill.router)
|
|
app.include_router(search.router)
|
|
app.include_router(chats.router)
|
|
app.include_router(media.router)
|
|
app.include_router(avatars.router)
|
|
app.include_router(custom_emoji.router)
|
|
app.include_router(social.router)
|
|
app.include_router(presence.router)
|
|
app.include_router(stories.router)
|
|
app.include_router(profile.router)
|
|
app.include_router(events.router)
|
|
app.include_router(peers.router)
|
|
app.include_router(discover.router)
|
|
app.include_router(annotations.router)
|
|
app.include_router(watches.router)
|
|
app.include_router(shares.router)
|
|
app.include_router(storage.router)
|
|
app.include_router(files.router)
|
|
|
|
app.mount("/mcp", mcp_app)
|
|
|
|
|
|
@app.api_route("/mcp", methods=["GET", "POST", "DELETE"])
|
|
async def mcp_trailing_slash(request: Request) -> RedirectResponse:
|
|
query = request.url.query
|
|
return RedirectResponse(f"/mcp/?{query}" if query else "/mcp/", status_code=307)
|
|
|
|
|
|
_spa_dir = Path(env.api.static_dir).resolve()
|
|
if _spa_dir.is_dir():
|
|
_spa_index = _spa_dir / "index.html"
|
|
|
|
@app.get("/{spa_path:path}")
|
|
async def serve_spa(spa_path: str) -> FileResponse:
|
|
candidate = (_spa_dir / spa_path).resolve()
|
|
if spa_path and candidate.is_relative_to(_spa_dir) and candidate.is_file():
|
|
immutable = spa_path.startswith("_app/immutable/")
|
|
return FileResponse(
|
|
candidate, headers=IMMUTABLE_HEADERS if immutable else DAY_HEADERS
|
|
)
|
|
return FileResponse(_spa_index, headers=NO_STORE_HEADERS)
|
|
|
|
|
|
app.add_middleware(BearerAuthMiddleware, token=_token)
|
|
|
|
setup_dishka(container, app)
|