from urllib.parse import quote from fastapi import APIRouter, Depends from fastapi.responses import RedirectResponse from pydantic import BaseModel, Field from sqlalchemy.orm import Session from .auth import current_user from .config import settings from .database import get_db from .meta_service import ( MetaApiError, analytics, authorization_url, disconnect, connect_with_user_token, finish_oauth, http_meta, list_ad_accounts, list_ads, list_adsets, list_businesses, campaign_detail, list_campaigns, list_pages, overview, page_detail, public_connection, publish_campaign, load_connection, recent_audit, select_ad_account, select_page, set_ad_status, set_campaign_status, settings_view, update_ad, update_campaign, ) from .models import UserAccount router = APIRouter(prefix="/api/v1/meta", tags=["meta"]) class Confirm(BaseModel): confirm: bool = False class ManualToken(BaseModel): access_token: str = Field(min_length=1, max_length=4096) class IdBody(Confirm): id: str = Field(min_length=1, max_length=64) class StatusBody(Confirm): status: str class CampaignEdit(Confirm): name: str | None = None daily_budget: float | None = Field(default=None, gt=0, le=10_000_000) class CampaignPublish(Confirm): name: str = Field(min_length=1, max_length=120) objective: str destination: str page_id: str daily_budget: float = Field(gt=0, le=10_000_000) countries: list[str] = Field(min_length=1, max_length=5) age_min: int = Field(default=18, ge=18, le=65) age_max: int = Field(default=65, ge=18, le=65) message: str = Field(min_length=1, max_length=2000) headline: str = "" link: str = "" call_to_action: str = "LEARN_MORE" activate: bool = False class AdEdit(Confirm): name: str | None = None message: str | None = None headline: str | None = None call_to_action: str | None = None link: str | None = None def _confirmed(body: Confirm) -> None: if not body.confirm: raise http_meta(MetaApiError(422, "validation", "Confirm this action before Urmi sends it to Meta.")) def _redirect(query: str) -> RedirectResponse: origin = settings.frontend_origin.rstrip("/") return RedirectResponse(f"{origin}/console/meta/connections?{query}", status_code=302) @router.get("/status") def status(user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: conn = load_connection(db, user) visible = conn if conn and conn.status != "disconnected" else None return public_connection(visible) @router.get("/oauth/start") def oauth_start(user: UserAccount = Depends(current_user)) -> dict: try: return {"authorization_url": authorization_url(user)} except MetaApiError as exc: raise http_meta(exc) from exc @router.get("/oauth/callback") async def oauth_callback(code: str = "", state: str = "", error: str = "", db: Session = Depends(get_db)): if error or not code or not state: return _redirect("error=Meta%20Login%20was%20cancelled") try: await finish_oauth(db, code, state) except MetaApiError as exc: return _redirect(f"error={quote(exc.message)}") except Exception: return _redirect("error=Meta%20Login%20could%20not%20be%20completed") return _redirect("connected=1") @router.post("/token") async def save_token(body: ManualToken, user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: try: return await connect_with_user_token(db, user, body.access_token) except MetaApiError as exc: raise http_meta(exc) from exc @router.post("/disconnect") def disconnect_meta(body: Confirm, user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: _confirmed(body) try: disconnect(db, user) except MetaApiError as exc: raise http_meta(exc) from exc return {"status": "disconnected"} @router.get("/overview") async def meta_overview(user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: return await overview(db, user) @router.get("/businesses") async def businesses(user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: try: return await list_businesses(db, user) except MetaApiError as exc: raise http_meta(exc) from exc @router.get("/pages") async def pages(user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: try: return await list_pages(db, user) except MetaApiError as exc: raise http_meta(exc) from exc @router.get("/pages/{page_id}") async def page(page_id: str, user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: try: return await page_detail(db, user, page_id) except MetaApiError as exc: raise http_meta(exc) from exc @router.post("/pages/select") async def choose_page(body: IdBody, user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: _confirmed(body) try: return await select_page(db, user, body.id) except MetaApiError as exc: raise http_meta(exc) from exc @router.get("/ad-accounts") async def ad_accounts(user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: try: return await list_ad_accounts(db, user) except MetaApiError as exc: raise http_meta(exc) from exc @router.post("/ad-accounts/select") async def choose_ad_account(body: IdBody, user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: _confirmed(body) try: return await select_ad_account(db, user, body.id) except MetaApiError as exc: raise http_meta(exc) from exc @router.get("/campaigns") async def campaigns(user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: try: return await list_campaigns(db, user) except MetaApiError as exc: raise http_meta(exc) from exc @router.get("/campaigns/{campaign_id}") async def campaign(campaign_id: str, user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: try: return await campaign_detail(db, user, campaign_id) except MetaApiError as exc: raise http_meta(exc) from exc @router.get("/adsets") async def adsets( campaign_id: str | None = None, user: UserAccount = Depends(current_user), db: Session = Depends(get_db), ) -> dict: try: return await list_adsets(db, user, campaign_id or None) except MetaApiError as exc: raise http_meta(exc) from exc @router.post("/campaigns") async def create_campaign(body: CampaignPublish, user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: _confirmed(body) try: return await publish_campaign(db, user, body.model_dump()) except MetaApiError as exc: raise http_meta(exc) from exc @router.post("/campaigns/{campaign_id}") async def edit_campaign( campaign_id: str, body: CampaignEdit, user: UserAccount = Depends(current_user), db: Session = Depends(get_db), ) -> dict: _confirmed(body) try: return await update_campaign(db, user, campaign_id, body.name, body.daily_budget) except MetaApiError as exc: raise http_meta(exc) from exc @router.post("/campaigns/{campaign_id}/status") async def campaign_status( campaign_id: str, body: StatusBody, user: UserAccount = Depends(current_user), db: Session = Depends(get_db), ) -> dict: _confirmed(body) try: return await set_campaign_status(db, user, campaign_id, body.status) except MetaApiError as exc: raise http_meta(exc) from exc @router.get("/ads") async def ads(user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: try: return await list_ads(db, user) except MetaApiError as exc: raise http_meta(exc) from exc @router.post("/ads/{ad_id}") async def edit_ad(ad_id: str, body: AdEdit, user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: _confirmed(body) try: return await update_ad(db, user, ad_id, body.model_dump()) except MetaApiError as exc: raise http_meta(exc) from exc @router.post("/ads/{ad_id}/status") async def ad_status( ad_id: str, body: StatusBody, user: UserAccount = Depends(current_user), db: Session = Depends(get_db), ) -> dict: _confirmed(body) try: return await set_ad_status(db, user, ad_id, body.status) except MetaApiError as exc: raise http_meta(exc) from exc @router.get("/analytics") async def meta_analytics( date_preset: str = "last_30d", user: UserAccount = Depends(current_user), db: Session = Depends(get_db), ) -> dict: try: return await analytics(db, user, date_preset) except MetaApiError as exc: raise http_meta(exc) from exc @router.get("/audit") def audit(user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: return {"events": recent_audit(db, user)} @router.get("/settings") def meta_settings(user: UserAccount = Depends(current_user), db: Session = Depends(get_db)) -> dict: return settings_view(db, user)