from __future__ import annotations import json from pathlib import Path from uuid import uuid4 from fastapi import APIRouter, BackgroundTasks, File, HTTPException, UploadFile from pydantic import BaseModel, Field from app.db import fetch_all, fetch_one from app.services.social_publish import PLATFORMS, get_configured_channels, run_publish_job router = APIRouter(prefix="/api/marketing", tags=["marketing-api"]) BASE_DIR = Path(__file__).resolve().parent.parent.parent UPLOAD_DIR = BASE_DIR / "static" / "uploads" / "marketing" def _serialize_row(row: dict | None) -> dict | None: if not row: return None out = dict(row) for key, value in list(out.items()): if hasattr(value, "isoformat"): out[key] = value.isoformat() return out def _serialize_rows(rows: list[dict]) -> list[dict]: return [_serialize_row(row) for row in rows] class PublishRequest(BaseModel): text: str = Field(..., min_length=1, max_length=5000) image_url: str | None = None media_ids: list[int] = Field(default_factory=list) channels: list[str] = Field(default_factory=list) @router.post("/upload") async def upload_marketing_media(file: UploadFile = File(...)) -> dict: data = await file.read() if not data: raise HTTPException(status_code=400, detail="empty file") UPLOAD_DIR.mkdir(parents=True, exist_ok=True) ext = Path(file.filename or "upload.bin").suffix or ".bin" filename = f"{uuid4().hex}{ext.lower()}" path = UPLOAD_DIR / filename path.write_bytes(data) media_url = f"/static/uploads/marketing/{filename}" row = fetch_one( """ INSERT INTO marketing_media (filename, original_name, file_path, media_url, mime_type, size_bytes, created_at) VALUES (%s, %s, %s, %s, %s, %s, NOW()) RETURNING id """, ( filename, file.filename or filename, str(path), media_url, file.content_type or "application/octet-stream", len(data), ), ) return {"media_id": row["id"], "url": media_url} @router.post("/publish", status_code=202) def create_publish_job(body: PublishRequest, background_tasks: BackgroundTasks) -> dict: channels = [c.strip().lower() for c in body.channels if c.strip()] invalid = [c for c in channels if c not in PLATFORMS] if invalid: raise HTTPException(status_code=400, detail=f"Unsupported channels: {', '.join(invalid)}") if not channels: channels = list(PLATFORMS) row = fetch_one( """ INSERT INTO social_publish_jobs (text, image_url, media_ids, channels, status, created_at, updated_at) VALUES (%s, %s, %s::jsonb, %s::jsonb, %s, NOW(), NOW()) RETURNING id """, ( body.text, body.image_url, json.dumps(body.media_ids), json.dumps(channels), "queued", ), ) job_id = int(row["id"]) background_tasks.add_task(run_publish_job, job_id, body.text, channels, body.image_url, body.media_ids) return {"job_id": job_id, "status": "queued", "channels": channels} @router.get("/publish/{job_id}") def get_publish_job(job_id: int) -> dict: row = fetch_one("SELECT * FROM social_publish_jobs WHERE id = %s", (job_id,)) if not row: raise HTTPException(status_code=404, detail="Job not found") return {"job": _serialize_row(row)} @router.get("/publish/history") def list_publish_history(limit: int = 50) -> dict: safe_limit = max(1, min(limit, 200)) rows = fetch_all( "SELECT * FROM social_publish_jobs ORDER BY created_at DESC LIMIT %s", (safe_limit,), ) return {"items": _serialize_rows(rows), "count": len(rows)} @router.get("/channels") def list_channels() -> dict: items = get_configured_channels() return {"items": items, "platforms": list(PLATFORMS)}