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>
133 lines
4.1 KiB
Python
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}
|