dp-zp-agent/app/main.py

157 lines
4.1 KiB
Python

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"],
}