This commit is contained in:
parent
d1149ff471
commit
170ec64cec
@ -1,44 +1,126 @@
|
||||
# /home/polisplexity/polisplexity/pxy_meta_pages/views.py
|
||||
from django.http import JsonResponse, HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
import json
|
||||
import logging
|
||||
from django.conf import settings
|
||||
from .webhook_handlers import verify_webhook_token, parse_webhook_payload, handle_comment_event, handle_share_event
|
||||
import json, logging, hmac, hashlib
|
||||
|
||||
from .webhook_handlers import (
|
||||
verify_webhook_token, # keep using your app’s style if you already have it
|
||||
parse_webhook_payload, # your existing parser for Page feed "changes"
|
||||
handle_comment_event, # existing
|
||||
handle_share_event, # existing
|
||||
handle_message_event, # NEW (you’ll add it below)
|
||||
handle_postback_event, # NEW (you’ll add it below)
|
||||
)
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VERIFY_TOKEN = settings.VERIFY_TOKEN
|
||||
VERIFY_TOKEN = getattr(settings, "VERIFY_TOKEN", "")
|
||||
APP_SECRET = getattr(settings, "APP_SECRET", "") # add this in settings/.env
|
||||
VERIFY_SIG = getattr(settings, "VERIFY_SIGNATURE", True) # optional toggle (default True)
|
||||
|
||||
def _valid_signature(raw_body: bytes, header: str) -> bool:
|
||||
"""
|
||||
Verify X-Hub-Signature-256 header using APP_SECRET (recommended by Meta).
|
||||
"""
|
||||
if not VERIFY_SIG:
|
||||
return True
|
||||
if not APP_SECRET:
|
||||
logger.warning("APP_SECRET not set; skipping signature verification.")
|
||||
return True
|
||||
if not header or not header.startswith("sha256="):
|
||||
logger.warning("Missing/invalid X-Hub-Signature-256 header.")
|
||||
return False
|
||||
received = header.split("=", 1)[1]
|
||||
expected = hmac.new(APP_SECRET.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
|
||||
ok = hmac.compare_digest(received, expected)
|
||||
if not ok:
|
||||
logger.warning("Signature mismatch on webhook payload.")
|
||||
return ok
|
||||
|
||||
@csrf_exempt
|
||||
def facebook_webhook(request):
|
||||
"""
|
||||
Handles incoming webhook requests from Facebook.
|
||||
One endpoint for:
|
||||
- GET: webhook verification (Messenger)
|
||||
- POST: Page feed changes (comment/share) and Messenger events (messages/postbacks)
|
||||
"""
|
||||
if request.method == "POST":
|
||||
try:
|
||||
logger.info("Received POST request.")
|
||||
payload = json.loads(request.body)
|
||||
data = parse_webhook_payload(payload)
|
||||
# --- GET VERIFICATION (fixes 405) ---
|
||||
if request.method == "GET":
|
||||
mode = request.GET.get("hub.mode")
|
||||
token = request.GET.get("hub.verify_token")
|
||||
challenge = request.GET.get("hub.challenge", "")
|
||||
# Use your helper if you prefer: verify_webhook_token(token)
|
||||
if mode == "subscribe" and token == VERIFY_TOKEN:
|
||||
return HttpResponse(challenge, status=200)
|
||||
return HttpResponse("Forbidden", status=403)
|
||||
|
||||
sender_id = data.get("from", {}).get("id")
|
||||
page_id = payload.get("entry", [{}])[0].get("id")
|
||||
# --- EVENTS ---
|
||||
if request.method == "POST":
|
||||
raw = request.body or b""
|
||||
# Verify signature (recommended)
|
||||
sig = request.META.get("HTTP_X_HUB_SIGNATURE_256")
|
||||
if not _valid_signature(raw, sig):
|
||||
return JsonResponse({"error": "Invalid signature"}, status=403)
|
||||
|
||||
try:
|
||||
payload = json.loads(raw.decode("utf-8") or "{}")
|
||||
entries = payload.get("entry", [])
|
||||
for entry in entries:
|
||||
page_id = entry.get("id")
|
||||
keys = list(entry.keys())
|
||||
print(f"[WEBHOOK] entry0.keys={keys}", flush=True)
|
||||
|
||||
# ---- Messenger events ----
|
||||
if "messaging" in entry:
|
||||
for evt in entry.get("messaging", []):
|
||||
sender_id = (evt.get("sender") or {}).get("id")
|
||||
# Log similar to your current style
|
||||
print(f"[WEBHOOK] MESSENGER page={page_id} psid={sender_id}", flush=True)
|
||||
|
||||
# Message (avoid echo loops)
|
||||
msg = evt.get("message")
|
||||
if msg and not msg.get("is_echo"):
|
||||
# Let your handler decide what to do (persist PSID, enqueue reply, etc.)
|
||||
resp = handle_message_event(page_id, sender_id, msg)
|
||||
if resp is not None:
|
||||
return resp
|
||||
|
||||
# Postback (Get Started, buttons, persistent menu)
|
||||
postback = evt.get("postback")
|
||||
if postback:
|
||||
resp = handle_postback_event(page_id, sender_id, postback)
|
||||
if resp is not None:
|
||||
return resp
|
||||
|
||||
# ---- Page feed changes (your existing flow) ----
|
||||
if "changes" in entry:
|
||||
# Keep your existing parse + handlers untouched
|
||||
data = parse_webhook_payload(payload)
|
||||
sender = (data.get("from") or {}).get("id")
|
||||
item_type = data.get("item")
|
||||
# Optional: quick visibility
|
||||
print(f"[WEBHOOK] page_id={page_id} item_type={item_type!r}", flush=True)
|
||||
|
||||
if item_type == "share":
|
||||
return handle_share_event(page_id, data)
|
||||
resp = handle_share_event(page_id, data)
|
||||
if resp is not None:
|
||||
return resp
|
||||
elif item_type == "comment":
|
||||
return handle_comment_event(page_id, sender_id, data)
|
||||
resp = handle_comment_event(page_id, sender, data)
|
||||
if resp is not None:
|
||||
return resp
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON decoding error: {e}")
|
||||
# Acknowledge within 5s
|
||||
return JsonResponse({"status": "ok"}, status=200)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.exception("Invalid JSON payload")
|
||||
return JsonResponse({"error": "Invalid JSON payload"}, status=400)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing webhook: {e}")
|
||||
logger.exception("Error processing webhook")
|
||||
return JsonResponse({"error": str(e)}, status=500)
|
||||
|
||||
logger.info("POST request processed successfully.")
|
||||
return JsonResponse({"status": "success"}, status=200)
|
||||
|
||||
# Anything else
|
||||
logger.warning(f"Received unsupported HTTP method: {request.method}")
|
||||
return HttpResponse("Method Not Allowed", status=405)
|
||||
|
@ -7,6 +7,10 @@ from django.conf import settings
|
||||
from .services import FacebookService
|
||||
from .models import FacebookPageAssistant, EventType, FacebookEvent
|
||||
|
||||
import requests
|
||||
from pxy_openai.assistants import OpenAIAssistant as OpenAIService
|
||||
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -113,3 +117,66 @@ def handle_share_event(page_id, data):
|
||||
logger.error(f"Error posting comment on share via FacebookService: {e}")
|
||||
|
||||
return JsonResponse({"status": "share_logged"}, status=200)
|
||||
|
||||
|
||||
def handle_message_event(page_id: str, sender_psid: str, message: dict):
|
||||
"""
|
||||
Handles incoming Messenger messages:
|
||||
1) Log event to DB (like comments/shares)
|
||||
2) Generate AI reply with your configured OpenAI assistant for this page
|
||||
3) Send the reply via the Send API (using the page's access token)
|
||||
"""
|
||||
try:
|
||||
# 0) ignore echoes to prevent loops
|
||||
if not message or message.get("is_echo"):
|
||||
return JsonResponse({"status": "ignored"}, status=200)
|
||||
|
||||
text = (message.get("text") or "").strip()
|
||||
mid = message.get("mid") or ""
|
||||
|
||||
# 1) Persist event to DB (EventType 'message'; create if missing)
|
||||
try:
|
||||
page = FacebookPageAssistant.objects.get(page_id=page_id)
|
||||
et, _ = EventType.objects.get_or_create(code="message", defaults={"label": "Message"})
|
||||
FacebookEvent.objects.create(
|
||||
page=page,
|
||||
event_type=et,
|
||||
sender_id=sender_psid,
|
||||
object_id=mid,
|
||||
message=text
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error logging Messenger event: {e}")
|
||||
|
||||
# 2) Build prompt and get AI reply from the page's configured assistant
|
||||
try:
|
||||
page_assistant = FacebookPageAssistant.objects.get(page_id=page_id).assistant
|
||||
prompt = text if text else "Say hello and ask how you can help."
|
||||
openai_service = OpenAIService(name=page_assistant.name)
|
||||
bot_reply = openai_service.handle_message(prompt)
|
||||
except Exception as e:
|
||||
logger.exception(f"AI reply failed; falling back. Reason: {e}")
|
||||
bot_reply = "Gracias por tu mensaje 🙌. ¿En qué puedo ayudarte?"
|
||||
|
||||
# 3) Send the reply via Send API
|
||||
try:
|
||||
fb_service = FacebookService(PAGE_ACCESS_TOKEN) # reuse your token flow
|
||||
page_token = fb_service._get_page_access_token(page_id) or PAGE_ACCESS_TOKEN
|
||||
|
||||
url = "https://graph.facebook.com/v22.0/me/messages"
|
||||
payload = {
|
||||
"recipient": {"id": sender_psid},
|
||||
"messaging_type": "RESPONSE",
|
||||
"message": {"text": bot_reply},
|
||||
}
|
||||
resp = requests.post(url, params={"access_token": page_token}, json=payload, timeout=5)
|
||||
resp.raise_for_status()
|
||||
logger.info(f"Sent Messenger reply to psid={sender_psid}")
|
||||
return JsonResponse({"status": "message_replied"}, status=200)
|
||||
except requests.RequestException as e:
|
||||
logger.exception(f"Send API failed: {e}")
|
||||
return JsonResponse({"status": "send_failed"}, status=200)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"handle_message_event crashed: {e}")
|
||||
return JsonResponse({"status": "error", "detail": str(e)}, status=200)
|
||||
|
Loading…
x
Reference in New Issue
Block a user