5d60d33db1
Volledige Foodlinkk Command Center uitbreiding met social automatisering, reclamefolder filters, Proxmox monitoring en documentatie.
120 lines
3.8 KiB
Python
120 lines
3.8 KiB
Python
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)}
|