Files
Home_assistant/backend/app/api/routes/webhooks.py
T
2026-06-13 20:20:56 +00:00

107 lines
3.5 KiB
Python

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.chat.notice_inbox import post_notice_to_latest_chat
from app.config import get_settings
from app.db.base import get_db
from app.db.models import 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, user_id: int
) -> None:
if not results:
return
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']}")
post_notice_to_latest_chat("\n".join(lines), user_id)
@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, binding.user_id)
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, binding.user_id)
return {"ok": True, "results": results, "commits_processed": len(commits)}