|
|
|
from fastapi import APIRouter, Request, HTTPException, status |
|
from fastapi.responses import JSONResponse |
|
import logging |
|
import json |
|
from typing import Tuple, Optional |
|
|
|
|
|
from components.LLMs.Classifier import ZeroShotClassifier |
|
|
|
|
|
from components.handlers.whatsapp_handlers import ( |
|
handle_headlines, |
|
handle_preferences, |
|
handle_greeting, |
|
handle_help, |
|
handle_unsubscribe, |
|
handle_small_talk, |
|
handle_chat_question, |
|
) |
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
|
router = APIRouter() |
|
classifier = ZeroShotClassifier() |
|
|
|
def _extract_from_number_and_text(payload: dict) -> Tuple[Optional[str], Optional[str]]: |
|
""" |
|
Extracts (from_number, message_text) from a WhatsApp webhook payload. |
|
Returns (None, None) if not a user message (e.g., statuses, billing-event). |
|
""" |
|
try: |
|
entries = payload.get("entry", []) |
|
for entry in entries: |
|
changes = entry.get("changes", []) |
|
for change in changes: |
|
field = change.get("field") |
|
|
|
if field != "messages": |
|
continue |
|
|
|
value = change.get("value", {}) or {} |
|
|
|
messages_list = value.get("messages", []) |
|
if not messages_list: |
|
|
|
statuses = value.get("statuses", []) |
|
if statuses: |
|
|
|
pass |
|
continue |
|
|
|
|
|
msg = messages_list[0] |
|
from_number = msg.get("from") |
|
mtype = msg.get("type") |
|
|
|
|
|
if mtype == "text": |
|
text_body = (msg.get("text", {}) or {}).get("body") |
|
if from_number and text_body: |
|
return from_number, text_body |
|
|
|
|
|
if mtype == "button": |
|
b = msg.get("button", {}) or {} |
|
intent = b.get("payload") or b.get("text") |
|
if from_number and intent: |
|
return from_number, intent |
|
|
|
|
|
if mtype == "interactive": |
|
i = msg.get("interactive", {}) or {} |
|
if "button_reply" in i: |
|
intent = i["button_reply"].get("id") or i["button_reply"].get("title") |
|
if from_number and intent: |
|
return from_number, intent |
|
if "list_reply" in i: |
|
intent = i["list_reply"].get("id") or i["list_reply"].get("title") |
|
if from_number and intent: |
|
return from_number, intent |
|
|
|
|
|
return None, None |
|
|
|
except Exception as e: |
|
|
|
logging.exception(f"_extract_from_number_and_text error: {e}") |
|
return None, None |
|
|
|
@router.post("/message-received") |
|
async def whatsapp_webhook_receiver(request: Request): |
|
try: |
|
body_str = (await request.body()).decode("utf-8") |
|
logging.info(f"Raw webhook body received: {body_str}") |
|
|
|
try: |
|
incoming_message = json.loads(body_str) |
|
except json.JSONDecodeError: |
|
logging.error("❌ Failed to decode webhook body as JSON") |
|
return JSONResponse(status_code=400, content={"error": "Invalid JSON format"}) |
|
|
|
from_number, message_text = _extract_from_number_and_text(incoming_message) |
|
if not from_number or not message_text: |
|
logging.info("Ignoring non-message webhook or missing sender/text.") |
|
return JSONResponse(status_code=200, content={"status": "ignored", "message": "No user message"}) |
|
|
|
logging.info(f"Message from {from_number}: {message_text}") |
|
|
|
normalized = message_text.lower().strip().replace("’", "'") |
|
if normalized == "view today's headlines": |
|
return handle_headlines(from_number) |
|
|
|
try: |
|
label, meta = classifier.classify(message_text) |
|
logging.info(f"Intent classified: {label} | meta={meta}") |
|
except Exception as e: |
|
logging.exception(f"Classifier error: {e}") |
|
if any(k in normalized for k in ["headline", "digest", "news", "today"]): |
|
return handle_headlines(from_number) |
|
return handle_help(from_number) |
|
|
|
if label == "headlines_request": |
|
return handle_headlines(from_number) |
|
elif label == "preferences_update": |
|
return handle_preferences(from_number) |
|
elif label == "greeting": |
|
return handle_greeting(from_number) |
|
elif label == "help": |
|
return handle_help(from_number) |
|
elif label == "unsubscribe": |
|
return handle_unsubscribe(from_number) |
|
elif label == "small_talk": |
|
return handle_small_talk(from_number) |
|
elif label == "chat_question": |
|
return handle_chat_question(from_number, message_text) |
|
else: |
|
return handle_help(from_number) |
|
|
|
except Exception as e: |
|
logging.error(f"Error processing webhook: {e}", exc_info=True) |
|
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)}) |
|
|
|
|
|
@router.get("/message-received") |
|
async def whatsapp_webhook_verify(request: Request): |
|
mode = request.query_params.get("hub.mode") |
|
challenge = request.query_params.get("hub.challenge") |
|
|
|
if mode == "subscribe" and challenge: |
|
logging.info(f"Webhook verification successful. Challenge: {challenge}") |
|
return JSONResponse(status_code=200, content=int(challenge)) |
|
else: |
|
logging.warning(f"Webhook verification failed. Mode: {mode}, Challenge: {challenge}") |
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Verification failed") |
|
|