"""meta - stateless API proxy for the Meta Marketing API (Facebook/Instagram). Runs behind the AppFactory Caddy reverse proxy at /apps/. ROOT_PATH is injected as an env var; FastAPI's ``root_path`` makes Swagger UI and the OpenAPI ``servers`` use the proxy prefix so "Try it out" hits /apps/meta/... . The service stores no secrets. Every credential is supplied per request in an X- header and used only to talk to the Graph API (see AGENTS.md and ``app.credentials``). Phase 1 is read-only. """ import os from fastapi import FastAPI, Request from . import config from .clients.graph import current_usage from .errors import register_exception_handlers from .logging_config import get_logger from .routers import entities, infra, insights, passthrough logger = get_logger(__name__) ROOT_PATH = os.getenv("ROOT_PATH", "") DESCRIPTION = f""" Stateless proxy nad **Meta Marketing API** (Facebook / Instagram reklamy). Veškeré přihlašovací údaje se posílají v každém requestu jako `X-` hlavičky – služba si nic neukládá. Vyplníte je v Swaggeru po kliknutí na **Try it out**. | Hlavička | Povinné | Význam | | --- | --- | --- | | `X-Meta-Access-Token` | ano* | Access token (doporučen System User token z Business Manageru). | | `Authorization: Bearer ` | ano* | Rovnocenná alternativa k `X-Meta-Access-Token`. | | `X-Meta-App-Secret` | ne, ale doporučeno | App secret – proxy z něj dopočítá `appsecret_proof`. | | `X-Meta-Api-Version` | ne | Přepsání verze Graph API pro daný request, např. `v25.0`. | \\* Token je povinný; pošlete ho buď v `X-Meta-Access-Token`, nebo ve standardní hlavičce `Authorization: Bearer`. Pokud pošlete obě, vyhrává `X-Meta-Access-Token`. --- ## Verze Graph API Výchozí verze je **{config.META_API_VERSION}** (env `META_API_VERSION`). Jednotlivý request ji může přepsat hlavičkou `X-Meta-Api-Version` – upgrade na novou verzi Graphu tedy nevyžaduje deploy. Formát se validuje (`vNN.N`); volitelně lze službu zamknout na seznam ověřených verzí přes `META_ALLOWED_API_VERSIONS`. ## Kde vzít přihlašovací údaje ### 🔹 Access token – System User (doporučeno) 1. [Business Manager](https://business.facebook.com/) → **Nastavení firmy → Uživatelé → Systémoví uživatelé**. 2. **Přidat** systémového uživatele, role *Admin* nebo *Zaměstnanec*. 3. **Přidat aktiva** → vyberte reklamní účty, se kterými má pracovat, a udělte mu na nich oprávnění *Správa kampaní*. 4. **Vygenerovat nový token** → vyberte aplikaci a oprávnění (viz níže). Token **negenerujte s expirací**, pokud chcete trvalou platnost. > 💡 System User token nepřestane fungovat, když někdo odejde z firmy nebo si > změní heslo – na rozdíl od uživatelského OAuth tokenu. Proto je pro > server-to-server integraci vhodnější. Potřebná oprávnění (scopes) pro tuto fázi (jen čtení): `ads_read`, `business_management`. ### 🔹 App secret (`X-Meta-App-Secret`) [developers.facebook.com](https://developers.facebook.com/apps/) → vaše aplikace → **Nastavení → Základní → App Secret**. Pokud má aplikace zapnuté *Require app secret proof for server API calls* (Nastavení → Pokročilé), **je tato hlavička nutná** – bez ní Meta volání odmítne s chybou OAuth 100. Proxy z tokenu a app secretu spočítá `appsecret_proof` (HMAC-SHA256) při každém requestu; nikam ho neukládá ani neloguje. ### 🔹 ID reklamního účtu Business Manager → **Nastavení firmy → Reklamní účty**, nebo v Ads Manageru vlevo nahoře. Číslo lze posílat s prefixem i bez (`act_123456789` i `123456789`). --- ## Limity a stránkování - Meta hlásí vyčerpání kvóty v hlavičkách `X-App-Usage`, `X-Ad-Account-Usage` a `X-Business-Use-Case-Usage`. Proxy je **propisuje zpět** do své odpovědi, takže si podle nich můžete řídit tempo volání. - Seznamy jsou stránkované kurzorem, ale **proxy je ve výchozím stavu projde za vás** (`all_pages=true`) a vrátí kompletní seznam – nemusíte řešit kurzory. Strop je `META_MAX_PAGES`; při jeho dosažení je v odpovědi `truncated: true`. Graph vrací 25 položek na stránku, takže větším `limit` ušetříte volání. S `all_pages=false` dostanete jednu syrovou stránku včetně `paging.cursors`. ## Velké reporty = asynchronně Rozsáhlé insights (dlouhé období, hodně breakdownů, celý účet na úrovni reklam) Meta počítá **asynchronně**. Použijte `POST /ads/insights/{{object_id}}/run`, který úlohu založí, počká na dokončení a vrátí data. Když se nestihne do limitu, dostanete `report_run_id` a doptáte se přes `/ads/insights/jobs/{{report_run_id}}`. --- Tato fáze je **jen pro čtení**. Zakládání a úpravy kampaní (fáze 2) záměrně nejsou zapojené – detaily v `documentation/` v repozitáři. """.strip() app = FastAPI( title=config.APP_NAME, version=config.APP_VERSION, description=DESCRIPTION, root_path=ROOT_PATH, ) register_exception_handlers(app) @app.middleware("http") async def _propagate_usage_headers(request: Request, call_next): """Echo Meta's rate-limit headers back to the caller. An empty dict is installed here and mutated by the Graph client (see ``app.clients.graph``); whatever it collected is copied onto the response. """ sink: dict[str, str] = {} current_usage.set(sink) response = await call_next(request) for header, value in sink.items(): response.headers[header] = value return response app.include_router(infra.router) app.include_router(entities.router) app.include_router(insights.router) app.include_router(passthrough.router) logger.info( "meta started (version=%s, root_path=%r, graph_api_version=%s)", config.APP_VERSION, ROOT_PATH, config.META_API_VERSION, )