Files
meta/app/main.py
T
JiriUhlir a7cb53e0e6 first
2026-07-20 07:36:55 +02:00

148 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""meta - stateless API proxy for the Meta Marketing API (Facebook/Instagram).
Runs behind the AppFactory Caddy reverse proxy at /apps/<app-id>. 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 <token>` | 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,
)