File size: 6,303 Bytes
3566f32 8d4ff93 3566f32 8d4ff93 3566f32 8d4ff93 5776f9f bbce544 3566f32 8d4ff93 f1e54f5 d202667 f1e54f5 d202667 bbce544 3566f32 bbce544 3566f32 bbce544 8d4ff93 3566f32 8d4ff93 3566f32 8d4ff93 3566f32 8d4ff93 3566f32 bbce544 3566f32 bbce544 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# routes/api/whatsapp_webhook.py
from fastapi import APIRouter, Request, HTTPException, status
from fastapi.responses import JSONResponse
import logging
import json
from typing import Tuple, Optional
# Classifier
from components.LLMs.Classifier import ZeroShotClassifier
# Handlers
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")
# Only process message-type changes
if field != "messages":
continue
value = change.get("value", {}) or {}
# Ignore pure status/billing updates that don't have 'messages'
messages_list = value.get("messages", [])
if not messages_list:
# Helpful debug so you can see why it was ignored
statuses = value.get("statuses", [])
if statuses:
# it's a status webhook, not a user message
pass
continue
# Only look at the first message in the list
msg = messages_list[0]
from_number = msg.get("from")
mtype = msg.get("type")
# 1) Plain text
if mtype == "text":
text_body = (msg.get("text", {}) or {}).get("body")
if from_number and text_body:
return from_number, text_body
# 2) Template reply button (older/simple schema)
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
# 3) Newer interactive replies (buttons or list)
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
# No usable user message found
return None, None
except Exception as e:
# Always return a tuple on error
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")
|