Files
antigravity-skills-reference/skills/whatsapp-cloud-api/assets/boilerplate/python/webhook_handler.py
ProgramadorBrasil 61ec71c5c7 feat: add 52 specialized AI agent skills (#217)
New skills covering 10 categories:

**Security & Audit**: 007 (STRIDE/PASTA/OWASP), cred-omega (secrets management)
**AI Personas**: Karpathy, Hinton, Sutskever, LeCun (4 sub-skills), Altman, Musk, Gates, Jobs, Buffett
**Multi-agent Orchestration**: agent-orchestrator, task-intelligence, multi-advisor
**Code Analysis**: matematico-tao (Terence Tao-inspired mathematical code analysis)
**Social & Messaging**: Instagram Graph API, Telegram Bot, WhatsApp Cloud API, social-orchestrator
**Image Generation**: AI Studio (Gemini), Stability AI, ComfyUI Gateway, image-studio router
**Brazilian Domain**: 6 auction specialist modules, 2 legal advisors, auctioneers data scraper
**Product & Growth**: design, invention, monetization, analytics, growth engine
**DevOps & LLM Ops**: Docker/CI-CD/AWS, RAG/embeddings/fine-tuning
**Skill Governance**: installer, sentinel auditor, context management

Each skill includes:
- Standardized YAML frontmatter (name, description, risk, source, tags, tools)
- Structured sections (Overview, When to Use, How it Works, Best Practices)
- Python scripts and reference documentation where applicable
- Cross-platform compatibility (Claude Code, Antigravity, Cursor, Gemini CLI, Codex CLI)

Co-authored-by: ProgramadorBrasil <214873561+ProgramadorBrasil@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:04:07 +01:00

133 lines
4.1 KiB
Python

"""WhatsApp Webhook Handler with HMAC-SHA256 validation."""
import hashlib
import hmac
import os
from functools import wraps
from typing import Any
from flask import Request, abort, request
def validate_hmac_signature(app_secret: str | None = None):
"""
Flask decorator to validate HMAC-SHA256 signature on webhook requests.
A Meta assina cada request com o App Secret no header X-Hub-Signature-256.
Validar esta assinatura previne requests falsificados.
Usa hmac.compare_digest para comparacao constant-time (previne timing attacks).
"""
secret = app_secret or os.environ["APP_SECRET"]
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
signature = request.headers.get("X-Hub-Signature-256", "")
if not signature:
abort(401, "Missing signature header")
raw_body = request.get_data()
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401, "Invalid signature")
return f(*args, **kwargs)
return decorated_function
return decorator
def verify_webhook(verify_token: str | None = None):
"""
Handle webhook verification (GET request from Meta).
Returns the challenge to confirm the webhook endpoint.
"""
token = verify_token or os.environ["VERIFY_TOKEN"]
mode = request.args.get("hub.mode")
req_token = request.args.get("hub.verify_token")
challenge = request.args.get("hub.challenge")
if mode == "subscribe" and req_token == token:
return challenge, 200
else:
abort(403, "Verification failed")
def parse_webhook_payload(data: dict[str, Any]) -> dict[str, list]:
"""
Parse webhook payload and extract messages and status updates.
Returns dict with 'messages' and 'statuses' lists.
"""
messages: list[dict] = []
statuses: list[dict] = []
for entry in data.get("entry", []):
for change in entry.get("changes", []):
value = change.get("value", {})
if "messages" in value:
messages.extend(value["messages"])
if "statuses" in value:
statuses.extend(value["statuses"])
return {"messages": messages, "statuses": statuses}
def extract_message_content(message: dict[str, Any]) -> dict[str, Any]:
"""
Extract readable content from an incoming message.
Returns dict with 'type' and relevant fields (text, button_id, media_id, etc.).
"""
msg_type = message.get("type", "unknown")
if msg_type == "text":
return {"type": "text", "text": message.get("text", {}).get("body")}
if msg_type == "interactive":
interactive = message.get("interactive", {})
int_type = interactive.get("type")
if int_type == "button_reply":
reply = interactive.get("button_reply", {})
return {"type": "button", "button_id": reply.get("id"), "text": reply.get("title")}
if int_type == "list_reply":
reply = interactive.get("list_reply", {})
return {"type": "list", "list_id": reply.get("id"), "text": reply.get("title")}
if int_type == "nfm_reply":
return {
"type": "flow",
"text": interactive.get("nfm_reply", {}).get("response_json"),
}
return {"type": "interactive"}
if msg_type in ("image", "document", "video", "audio"):
media_data = message.get(msg_type, {})
return {"type": msg_type, "media_id": media_data.get("id")}
if msg_type == "location":
loc = message.get("location", {})
return {
"type": "location",
"latitude": loc.get("latitude"),
"longitude": loc.get("longitude"),
}
if msg_type == "reaction":
reaction = message.get("reaction", {})
return {
"type": "reaction",
"emoji": reaction.get("emoji"),
"message_id": reaction.get("message_id"),
}
return {"type": msg_type}