157 lines
4.1 KiB
Python
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"],
|
|
}
|