feat: Ipad app production readiness, Colony orchestration, Social posting (#44)

#38 Ipad app production readiness, Colony orchestration, Social posting

Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local>
Reviewed-on: sagnik/Project_Velocity#44
This commit is contained in:
2026-05-03 18:30:38 +05:30
parent 59d398abc3
commit eeb684b46c
86 changed files with 20349 additions and 1655 deletions

View File

@@ -10,6 +10,8 @@ Routes:
POST /api/catalyst/auth/meta — OAuth token acquisition
"""
from __future__ import annotations
import os
import uuid
import hashlib
@@ -17,9 +19,11 @@ import logging
from typing import Any
from datetime import datetime
from fastapi import APIRouter, HTTPException, Query, Request, status
import httpx
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from pydantic import BaseModel, Field
from backend.auth.dependencies import UserPrincipal, get_current_user
from backend.services.ad_network_service import (
AdInsight,
BidStrategyUpdate,
@@ -27,6 +31,17 @@ from backend.services.ad_network_service import (
Platform,
ad_network_service,
)
from backend.services.social_posting import (
PostRequest,
PostStatus,
SocialPlatform,
SocialPostingConfigurationError,
SocialPostingError,
get_post,
list_posts,
publish_content,
publish_due_scheduled,
)
logger = logging.getLogger(__name__)
@@ -91,6 +106,13 @@ def _ok(data: Any, meta: dict | None = None) -> dict:
return {"status": "ok", "data": data, "meta": meta or {}}
def _get_db_pool(request: Request) -> Any:
pool = getattr(request.app.state, "db_pool", None)
if pool is None:
raise HTTPException(status_code=503, detail="Database unavailable.")
return pool
def _sha256_hash(value: str) -> str:
"""SHA-256 hash an email for Meta's hashed audience upload."""
return hashlib.sha256(value.strip().lower().encode()).hexdigest()
@@ -510,3 +532,91 @@ async def meta_oauth(payload: MetaAuthRequest) -> dict:
"token_type": token_data.get("token_type", "bearer"),
"expires_in": token_data.get("expires_in"),
})
# ── 6. Social publishing ─────────────────────────────────────────────────────
@router.post("/publish", status_code=status.HTTP_201_CREATED, summary="Publish or schedule content to social channels")
async def api_publish_content(
payload: PostRequest,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict:
try:
result = await publish_content(
pool=_get_db_pool(request),
tenant_id=user.tenant_id,
actor_id=user.user_id,
payload=payload,
)
except SocialPostingConfigurationError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=422, detail=f"Invalid schedule_time: {exc}") from exc
except (SocialPostingError, httpx.HTTPError) as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return _ok(result)
@router.get("/posts", summary="List tenant-scoped social posts")
async def api_list_social_posts(
request: Request,
platform: SocialPlatform | None = Query(default=None),
post_status: PostStatus | None = Query(default=None, alias="status"),
limit: int = Query(default=50, ge=1, le=200),
user: UserPrincipal = Depends(get_current_user),
) -> dict:
posts = await list_posts(
pool=_get_db_pool(request),
tenant_id=user.tenant_id,
platform=platform,
status=post_status,
limit=limit,
)
return _ok(posts, meta={"count": len(posts)})
@router.get("/posts/{post_id}", summary="Get a tenant-scoped social post")
async def api_get_social_post(
post_id: str,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict:
post = await get_post(pool=_get_db_pool(request), tenant_id=user.tenant_id, post_id=post_id)
if post is None:
raise HTTPException(status_code=404, detail=f"Social post '{post_id}' not found.")
return _ok(post)
@router.get("/scheduled", summary="List scheduled social posts for the authenticated tenant")
async def api_scheduled_posts(
request: Request,
limit: int = Query(default=50, ge=1, le=200),
user: UserPrincipal = Depends(get_current_user),
) -> dict:
posts = await list_posts(
pool=_get_db_pool(request),
tenant_id=user.tenant_id,
status=PostStatus.SCHEDULED,
limit=limit,
)
return _ok(posts, meta={"count": len(posts)})
@router.post("/scheduled/publish-due", summary="Publish due scheduled social posts for the authenticated tenant")
async def api_publish_due_scheduled(
request: Request,
limit: int = Query(default=20, ge=1, le=100),
user: UserPrincipal = Depends(get_current_user),
) -> dict:
try:
result = await publish_due_scheduled(
pool=_get_db_pool(request),
tenant_id=user.tenant_id,
limit=limit,
)
except SocialPostingConfigurationError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
except (SocialPostingError, httpx.HTTPError) as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return _ok(result)