This commit is contained in:
JiriUhlir
2026-07-20 07:36:55 +02:00
parent 496104f65a
commit a7cb53e0e6
16 changed files with 1856 additions and 19 deletions
+140 -18
View File
@@ -1,25 +1,147 @@
import os
from fastapi import FastAPI
"""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__)
APP_NAME = os.getenv("APP_NAME", "Meta services")
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
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=APP_NAME,
version=APP_VERSION,
root_path=ROOT_PATH
title=config.APP_NAME,
version=config.APP_VERSION,
description=DESCRIPTION,
root_path=ROOT_PATH,
)
@app.get("/health")
def health():
return {"status": "ok"}
register_exception_handlers(app)
@app.get("/version")
def version():
return {
"app": APP_NAME,
"version": APP_VERSION,
"language": "python",
"root_path": ROOT_PATH
}
@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,
)