From 170ec64cec51dd11d67964c5b3e24020e086f9c3 Mon Sep 17 00:00:00 2001 From: Ekaropolus Date: Sun, 7 Sep 2025 02:58:02 -0600 Subject: [PATCH] Messenger AI bot responder --- pxy_meta_pages/views.py | 126 ++++++++++++++++++++++++----- pxy_meta_pages/webhook_handlers.py | 67 +++++++++++++++ 2 files changed, 171 insertions(+), 22 deletions(-) diff --git a/pxy_meta_pages/views.py b/pxy_meta_pages/views.py index 972529b..54caa96 100644 --- a/pxy_meta_pages/views.py +++ b/pxy_meta_pages/views.py @@ -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) """ + # --- 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) + + # --- 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: - logger.info("Received POST request.") - payload = json.loads(request.body) - data = parse_webhook_payload(payload) + 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) - sender_id = data.get("from", {}).get("id") - page_id = payload.get("entry", [{}])[0].get("id") - item_type = data.get("item") + # ---- 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) - if item_type == "share": - return handle_share_event(page_id, data) - elif item_type == "comment": - return handle_comment_event(page_id, sender_id, data) + # 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 - except json.JSONDecodeError as e: - logger.error(f"JSON decoding error: {e}") + # 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": + resp = handle_share_event(page_id, data) + if resp is not None: + return resp + elif item_type == "comment": + resp = handle_comment_event(page_id, sender, data) + if resp is not None: + return resp + + # 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) diff --git a/pxy_meta_pages/webhook_handlers.py b/pxy_meta_pages/webhook_handlers.py index ec66a53..b3e954c 100644 --- a/pxy_meta_pages/webhook_handlers.py +++ b/pxy_meta_pages/webhook_handlers.py @@ -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)