import hashlib import hmac import json import logging from typing import Any from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy import select from sqlalchemy.orm import Session from app.config import get_settings from app.db.base import SessionLocal, get_db from app.db.models import ChatSession, Message, ProjectBinding from app.projects.service import ProjectService router = APIRouter() logger = logging.getLogger(__name__) def _verify_gitea_signature(body: bytes, signature: str | None, secret: str) -> bool: if not secret: return True if not signature: return False if signature.startswith("sha256="): signature = signature[7:] expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) def _post_close_notice(results: list[dict[str, Any]], owner: str, repo: str) -> None: if not results: return db = SessionLocal() try: session = db.scalar( select(ChatSession).order_by(ChatSession.updated_at.desc()).limit(1) ) if not session: session = ChatSession(title="Git") db.add(session) db.commit() db.refresh(session) lines = [f"🔀 **Push** `{owner}/{repo}`"] for item in results: if "closed" in item: lines.append(f"- `{item.get('commit', '?')}`: закрыто {item['closed']}") elif "error" in item: lines.append(f"- ошибка: {item['error']}") db.add(Message(session_id=session.id, role="notice", content="\n".join(lines))) db.commit() finally: db.close() @router.post("/webhooks/gitea") async def gitea_webhook(request: Request, db: Session = Depends(get_db)) -> dict[str, Any]: body = await request.body() settings = get_settings() signature = ( request.headers.get("X-Gitea-Signature") or request.headers.get("X-Gogs-Signature") or request.headers.get("X-Hub-Signature-256") ) if not _verify_gitea_signature(body, signature, settings.gitea_webhook_secret): raise HTTPException(status_code=401, detail="Invalid webhook signature") payload = json.loads(body) if payload.get("secret") and settings.gitea_webhook_secret: if payload.get("secret") != settings.gitea_webhook_secret: raise HTTPException(status_code=401, detail="Invalid webhook secret") event = request.headers.get("X-Gitea-Event", "") if event != "push": return {"ok": True, "skipped": event} repo = payload.get("repository", {}) owner = repo.get("owner", {}).get("login", "") repo_name = repo.get("name", "") if not owner or not repo_name: raise HTTPException(status_code=400, detail="Missing repository info") binding = db.scalar( select(ProjectBinding).where( ProjectBinding.gitea_owner == owner, ProjectBinding.gitea_repo == repo_name, ) ) if not binding: return {"ok": True, "skipped": "unknown repo"} commits = list(payload.get("commits") or []) if not commits: head = payload.get("head_commit") if head: commits = [head] logger.info( "Gitea push %s/%s ref=%s commits=%d", owner, repo_name, payload.get("ref", ""), len(commits), ) service = ProjectService(db) results = service.process_push(owner, repo_name, commits) if results: logger.info("Gitea push results: %s", results) else: logger.warning("Gitea push: no close actions for %s/%s", owner, repo_name) _post_close_notice(results, owner, repo_name) return {"ok": True, "results": results, "commits_processed": len(commits)}