""" 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})