from fastapi import FastAPI, Request, Header, HTTPException from pydantic import BaseModel from typing import Optional, Any, Dict import requests import os app = FastAPI() # --- Configuration --- DISCORD_WEBHOOK_URL = os.environ.get("DISCORD_WEBHOOK_URL") WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET") # --- Models --- class HFRepo(BaseModel): type: str name: str id: str url: Dict[str, str] class HFEvent(BaseModel): action: str scope: str class HFPayload(BaseModel): repo: HFRepo event: HFEvent discussion: Optional[Dict[str, Any]] = None comment: Optional[Dict[str, Any]] = None webhook: Optional[Dict[str, Any]] = None @app.get("/") def home(): return {"status": "Webhook listener is running"} @app.post("/webhook") def handle_webhook( payload: HFPayload, x_webhook_secret: Optional[str] = Header(None) ): # 1. Security Check if WEBHOOK_SECRET: if not x_webhook_secret or x_webhook_secret.strip() != WEBHOOK_SECRET.strip(): print("Auth Failed") raise HTTPException(status_code=401, detail="Invalid Secret") # 2. Build Embed embed = build_discord_embed(payload) # 3. Send to Discord if DISCORD_WEBHOOK_URL: # Clean the URL just in case url = DISCORD_WEBHOOK_URL.strip().strip("'").strip('"') try: print(f"Sending to Discord: {payload.repo.name} -> {payload.event.action}") # requests is synchronous and handles DNS/Proxies much better in containers resp = requests.post(url, json={"embeds": [embed]}, timeout=10) resp.raise_for_status() print("Success: Message sent.") except Exception as e: print(f"Error sending to Discord: {e}") return {"message": "Error sending to Discord", "error": str(e)} return {"message": "Processed"} def build_discord_embed(data: HFPayload): # Basic data extraction repo_name = data.repo.name repo_url = data.repo.url.get('web', '') event_scope = data.event.scope event_action = data.event.action # Colors COLOR_BLUE = 0x3498db COLOR_GREEN = 0x2ecc71 COLOR_GREY = 0x95a5a6 color = COLOR_GREY title = f"Update on {repo_name}" description = f"Action: {event_action}\nScope: {event_scope}" # Smart Formatting if "repo.content" in event_scope: color = COLOR_BLUE title = f"New Commit to {repo_name}" description = f"The repository content has been updated.\n[View Changes]({repo_url})" elif "discussion" in event_scope: color = COLOR_GREEN disc_title = data.discussion.get('title', 'Discussion') if data.discussion else "Discussion" title = f"Discussion: {disc_title}" if data.comment: content = data.comment.get('content', '') if len(content) > 200: content = content[:200] + "..." description = f"**New Comment:**\n{content}\n\n[View Discussion]({repo_url})" else: description = f"Discussion status updated: **{event_action}**" return { "title": title, "description": description, "url": repo_url, "color": color, "footer": {"text": "Hugging Face Webhooks"} }