148 lines
5.7 KiB
Python
148 lines
5.7 KiB
Python
"""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,
|
||
)
|