Ports meta-optimizer's Meta Ads execution/approval/creative-analysis layer (agent.py, meta_ads_client.py, baserow_client.py, slack_notifier.py, approval_server.py) and replaces the per-vertical CPL model with the PPL + monthly-capping-per-course model already used by leads-optimizer, via a new airtable_client.py that shares Cursos/Familias/CentroCurso/ CursoMes/Leads Lake with that project and adds Meta Ads Campaigns / MetaCampaignMes alongside its Google Ads Campaigns / GACampaignMes.
85 lines
2.6 KiB
Python
85 lines
2.6 KiB
Python
"""
|
|
FastAPI server that receives Slack interactive button callbacks (Approve / Reject).
|
|
|
|
Setup:
|
|
1. Create a Slack App, enable Interactivity, set Request URL to:
|
|
https://your-domain.com/slack/actions
|
|
2. Set SLACK_SIGNING_SECRET in your .env
|
|
3. Run: uvicorn approval_server:app --host 0.0.0.0 --port 3000
|
|
(for local dev: use ngrok to expose port 3000)
|
|
"""
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import time
|
|
import urllib.parse
|
|
|
|
from fastapi import FastAPI, Request, HTTPException
|
|
from fastapi.responses import JSONResponse
|
|
|
|
import config
|
|
from baserow_client import BaserowClient
|
|
import slack_notifier
|
|
|
|
app = FastAPI()
|
|
baserow = BaserowClient()
|
|
|
|
|
|
def _verify_slack_signature(body: bytes, timestamp: str, signature: str) -> bool:
|
|
if abs(time.time() - int(timestamp)) > 300:
|
|
return False
|
|
basestring = f"v0:{timestamp}:{body.decode()}"
|
|
computed = "v0=" + hmac.new(
|
|
config.SLACK_SIGNING_SECRET.encode(),
|
|
basestring.encode(),
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
return hmac.compare_digest(computed, signature)
|
|
|
|
|
|
@app.post("/slack/actions")
|
|
async def slack_actions(request: Request):
|
|
body = await request.body()
|
|
timestamp = request.headers.get("X-Slack-Request-Timestamp", "0")
|
|
signature = request.headers.get("X-Slack-Signature", "")
|
|
|
|
if not _verify_slack_signature(body, timestamp, signature):
|
|
raise HTTPException(status_code=401, detail="Invalid Slack signature")
|
|
|
|
form = urllib.parse.parse_qs(body.decode())
|
|
payload = json.loads(form.get("payload", ["{}"])[0])
|
|
|
|
actions = payload.get("actions", [])
|
|
if not actions:
|
|
return JSONResponse({"ok": True})
|
|
|
|
action = actions[0]
|
|
value = action.get("value", "") # "approve:42" or "reject:42"
|
|
channel = payload.get("channel", {}).get("id", config.SLACK_CHANNEL_ID)
|
|
message_ts = payload.get("message", {}).get("ts")
|
|
|
|
try:
|
|
verb, row_id_str = value.split(":", 1)
|
|
row_id = int(row_id_str)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail=f"Unexpected action value: {value}")
|
|
|
|
if verb == "approve":
|
|
baserow.update_action_status(row_id, "approved")
|
|
status_text = "aprobada"
|
|
elif verb == "reject":
|
|
baserow.update_action_status(row_id, "rejected")
|
|
status_text = "rechazada"
|
|
else:
|
|
raise HTTPException(status_code=400, detail=f"Unknown verb: {verb}")
|
|
|
|
if message_ts:
|
|
user = payload.get("user", {}).get("name", "unknown")
|
|
slack_notifier.update_message(
|
|
channel,
|
|
message_ts,
|
|
f"Accion {status_text} por {user}.",
|
|
)
|
|
|
|
return JSONResponse({"ok": True})
|