Lot 4/4 de l'A28 (god files) : l'ancien app/routers/board.py (2 101 lignes, 53 routes) devient le package `app/routers/board/` : - 12 modules de routes : pages 271 L (7 r.), page_api 229 (5), board_views 223 (8), page_ops 176 (3), sharing 175 (10), synced 144 (8), page_media 122 (4), import_ 85 (2), wiki 76 (2), library 66 (1), embed 64 (2), sync 51 (1) - _common.py (878 L) : 23 helpers dont 4 async + les 4 constantes (STATUS_COLORS, STATUS_LABELS, AI_KEYWORD_COLORS, _REPO_REF_RE) - __init__.py : __all__ complet — importateurs inchangés (api.py ×4 top-level, webhooks top-level, dashboard ×5 lazy, tests ×4) Preuve contractuelle : docs/openapi-v2.json régénéré = IDENTIQUE byte-à-byte (509 chemins, ordre préservé). Pièges rattrapés : - constantes d'état oubliées dans _common à la 1ʳᵉ passe (F821 + ImportError au chargement) → ré-insérées avec les valeurs exactes - docstring du header copié → F404 → slice [1:21] - helpers `async def` non détectés par `def ` seul A28 TERMINÉ en 4 lots : api_v2 (7.29.0), dashboard (7.30.0), collections (7.31.0), board (7.32.0) — 0 changement d'URL sur les 4. suite **1091/1091** · ruff OK · OpenAPI 509 identique · docs à jour
65 lines
2.0 KiB
Python
65 lines
2.0 KiB
Python
"""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}
|