"""FlowDeck — Board : embed. Découpe A28 de l'ancien app/routers/board.py (2 101 lignes, 53 routes) — un module par concern, contrat inchangé. """ from __future__ import annotations import logging from fastapi import APIRouter, Body, HTTPException, Request from app.config import settings from ._common import _REPO_REF_RE, _unfurl_repo logger = logging.getLogger(__name__) router = APIRouter(tags=["board"], prefix="/board") @router.post("/api/og/metadata") async def og_metadata(request: Request): """v5.5.0: Open Graph metadata for a bookmark card. v7.3.0: ``gitea:owner/repo`` and ``github:owner/repo`` schemes are unfurled straight from the forge API (no HTTP fetch of the HTML page). """ try: body = await request.json() except Exception: raise HTTPException(400, "Invalid JSON body") from None url = (body.get("url") or "").strip() if not url: raise HTTPException(400, "url required") m = _REPO_REF_RE.match(url) if m: forge, owner, repo = m.group(1).lower(), m.group(2), m.group(3) data = await _unfurl_repo(forge, owner, repo) if data: return {"ok": True, **data} from app.services.og_fetcher import fetch_og_metadata try: data = await fetch_og_metadata(url) except ValueError as exc: # A12 : SSRF — URL vers un hôte privé/loopback (ou redirection vers l'un). raise HTTPException(400, str(exc)) from None return {"ok": True, **data} @router.post("/api/embed/resolve") def resolve_embed(request: Request, body: dict = Body(...)): """v5.5.0: rewrite a pasted URL to its provider embed src. Powers the universal ``/embed`` block (YouTube, Vimeo, Figma, Maps, Docs, Loom, CodePen, Miro, Spotify, SoundCloud, Twitch, X/Twitter…). """ url = (body.get("url") or "").strip() if not url: raise HTTPException(400, "url required") from app.services.embeds import resolve_embed as _resolve data = _resolve(url, parent=settings.app_base_url) return {"ok": True, "url": url, **data}