From 9c9e634d65cdae754051270886e78521a3e8c5f6 Mon Sep 17 00:00:00 2001 From: Fam Zheng Date: Tue, 7 Apr 2026 10:13:04 +0100 Subject: [PATCH] auto-sync per-repo webhooks on startup + lifespan fix --- gitea.py | 41 +++++++++++++++++++++++++++++++++++++++++ main.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/gitea.py b/gitea.py index 099172e..916a681 100644 --- a/gitea.py +++ b/gitea.py @@ -114,6 +114,47 @@ class GiteaClient: return downloaded + async def list_user_repos(self, owner: str) -> list[dict]: + repos = [] + page = 1 + async with httpx.AsyncClient() as c: + while True: + r = await c.get( + f"{self.base}/users/{owner}/repos?page={page}&limit=50", + headers=self.headers, + ) + r.raise_for_status() + batch = r.json() + if not batch: + break + repos.extend(batch) + page += 1 + return repos + + async def ensure_webhook(self, owner: str, repo: str, webhook_url: str): + """Ensure a webhook exists for this repo pointing to webhook_url.""" + async with httpx.AsyncClient() as c: + r = await c.get( + f"{self.base}/repos/{owner}/{repo}/hooks", + headers=self.headers, + ) + if r.status_code == 200: + for h in r.json(): + if h.get("config", {}).get("url") == webhook_url: + return False # already exists + r = await c.post( + f"{self.base}/repos/{owner}/{repo}/hooks", + headers=self.headers, + json={ + "type": "gitea", + "active": True, + "events": ["issues", "issue_comment", "pull_request", "pull_request_comment"], + "config": {"url": webhook_url, "content_type": "json", "secret": ""}, + }, + ) + r.raise_for_status() + return True # created + async def _download_inline_images(self, texts: list[str], dest_dir: Path) -> list[dict]: downloaded = [] seen = set() diff --git a/main.py b/main.py index 16f9b9e..39a0432 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,8 @@ import hmac import logging import os +from contextlib import asynccontextmanager + from fastapi import FastAPI, Request, HTTPException import config @@ -13,12 +15,14 @@ from workspace import ensure_repo, repo_path, attachments_path logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger("gitea-bot") -app = FastAPI() +app = FastAPI(lifespan=lifespan) gitea = GiteaClient() ALLOWED_OWNER = "fam" MAX_RESPONSE_LEN = 60000 CLAUDE_TIMEOUT = 600 # 10 minutes +WEBHOOK_SYNC_INTERVAL = 300 # 5 minutes +WEBHOOK_URL = "http://100.65.168.42:9880/webhook" def verify_signature(payload: bytes, signature: str) -> bool: @@ -251,6 +255,33 @@ async def webhook(request: Request): return {"status": "skip", "reason": f"unhandled {event}/{action}"} +async def sync_webhooks(): + """Ensure all repos under ALLOWED_OWNER have our webhook.""" + try: + repos = await gitea.list_user_repos(ALLOWED_OWNER) + for r in repos: + name = r["name"] + created = await gitea.ensure_webhook(ALLOWED_OWNER, name, WEBHOOK_URL) + if created: + log.info(f"Added webhook to {ALLOWED_OWNER}/{name}") + except Exception: + log.exception("Failed to sync webhooks") + + +async def webhook_sync_loop(): + """Periodically sync webhooks for new repos.""" + while True: + await sync_webhooks() + await asyncio.sleep(WEBHOOK_SYNC_INTERVAL) + + +@asynccontextmanager +async def lifespan(app): + task = asyncio.create_task(webhook_sync_loop()) + yield + task.cancel() + + @app.get("/health") async def health(): return {"status": "ok"}