from __future__ import annotations import hashlib import hmac import json import os import sys from pathlib import Path from fastapi import FastAPI, Header, HTTPException, Request from pydantic import BaseModel, Field PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from scripts.common import DB_FILE, ZPWIKI_ROOT from scripts.rebuild_index import rebuild_index from scripts.search_utils import search_database WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "dev-secret") app = FastAPI( title="ZP Agent API", description="API pre vyhľadávanie v repozitári záverečných prác zpwiki.", version="0.4.0", ) class SearchRequest(BaseModel): query: str = Field(..., min_length=1) limit: int = Field(default=10, ge=1, le=50) class SyncRequest(BaseModel): pull_git: bool = Field( default=False, description="Ak je true, pred reindexovaním sa vykoná git pull v repozitári zpwiki.", ) def verify_gitea_signature(raw_body: bytes, signature: str | None) -> bool: if not signature: return False expected = hmac.new( WEBHOOK_SECRET.encode("utf-8"), raw_body, hashlib.sha256, ).hexdigest() signature = signature.strip() if signature.startswith("sha256="): signature = signature.replace("sha256=", "", 1) return hmac.compare_digest(expected, signature) def verify_simple_token(token: str | None) -> bool: if not token: return False return hmac.compare_digest(token, WEBHOOK_SECRET) @app.get("/health") def health() -> dict: return { "status": "ok", "database_exists": DB_FILE.exists(), "database_path": str(DB_FILE), "zpwiki_root": str(ZPWIKI_ROOT), "zpwiki_exists": ZPWIKI_ROOT.exists(), "webhook_secret_configured": bool(WEBHOOK_SECRET), } @app.post("/search") def search(request: SearchRequest) -> dict: try: mode, results = search_database( DB_FILE, request.query, request.limit, ) except FileNotFoundError as error: raise HTTPException(status_code=500, detail=str(error)) from error return { "query": request.query, "mode": mode, "count": len(results), "results": results, } @app.post("/sync") def sync(request: SyncRequest) -> dict: try: result = rebuild_index(pull_git=request.pull_git) except RuntimeError as error: raise HTTPException(status_code=500, detail=str(error)) from error return { "status": "ok", "pull_git": request.pull_git, "duration_seconds": result["duration_seconds"], "counts": result["counts"], } @app.post("/webhook/gitea") async def gitea_webhook( request: Request, x_gitea_event: str | None = Header(default=None, alias="X-Gitea-Event"), x_gitea_signature: str | None = Header(default=None, alias="X-Gitea-Signature"), x_gitea_token: str | None = Header(default=None, alias="X-Gitea-Token"), ) -> dict: raw_body = await request.body() signature_ok = verify_gitea_signature(raw_body, x_gitea_signature) token_ok = verify_simple_token(x_gitea_token) if not signature_ok and not token_ok: raise HTTPException( status_code=401, detail="Invalid webhook signature or token", ) try: payload = json.loads(raw_body.decode("utf-8")) if raw_body else {} except json.JSONDecodeError: payload = {} repository = payload.get("repository", {}) repository_name = repository.get("full_name") or repository.get("name") try: result = rebuild_index(pull_git=False) except RuntimeError as error: raise HTTPException(status_code=500, detail=str(error)) from error return { "status": "ok", "event": x_gitea_event or "unknown", "repository": repository_name, "verified_by": "signature" if signature_ok else "token", "duration_seconds": result["duration_seconds"], "counts": result["counts"], }