first
This commit is contained in:
+140
-18
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user