message handling changes
Browse files- routes/api/whatsapp_webhook.py +72 -29
routes/api/whatsapp_webhook.py
CHANGED
@@ -11,6 +11,67 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
|
|
11 |
|
12 |
router = APIRouter()
|
13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
# WhatsApp/Gupshup webhook endpoint
|
15 |
@router.post("/message-received")
|
16 |
async def whatsapp_webhook_receiver(request: Request):
|
@@ -30,40 +91,22 @@ async def whatsapp_webhook_receiver(request: Request):
|
|
30 |
logging.error("❌ Failed to decode webhook body as JSON")
|
31 |
return JSONResponse(status_code=400, content={"error": "Invalid JSON format"})
|
32 |
|
33 |
-
|
34 |
-
from_number =
|
35 |
-
message_text = None
|
36 |
-
|
37 |
-
# <<< FIX 2: Robustly extract data from the nested structure >>>
|
38 |
-
entries = incoming_message.get('entry', [])
|
39 |
-
for entry in entries:
|
40 |
-
changes = entry.get('changes', [])
|
41 |
-
for change in changes:
|
42 |
-
# We are interested in changes related to 'messages'
|
43 |
-
if change.get('field') == 'messages':
|
44 |
-
value = change.get('value', {})
|
45 |
-
messages_list = value.get('messages', [])
|
46 |
-
|
47 |
-
for msg in messages_list:
|
48 |
-
# Check if it's a text message and extract sender/body
|
49 |
-
if msg.get('type') == 'text':
|
50 |
-
from_number = msg.get('from')
|
51 |
-
message_text = msg.get('text', {}).get('body')
|
52 |
-
break # Found text message, exit inner loop
|
53 |
-
if from_number and message_text:
|
54 |
-
break # Found message, exit middle loop
|
55 |
-
if from_number and message_text:
|
56 |
-
break # Found message, exit outer loop
|
57 |
|
58 |
# Check if sender and message text were successfully extracted
|
59 |
if not from_number or not message_text:
|
60 |
-
|
61 |
-
|
|
|
62 |
|
63 |
logging.info(f"Message from {from_number}: {message_text}")
|
64 |
|
|
|
|
|
|
|
65 |
# Check for specific commands to send the digest
|
66 |
-
if
|
67 |
logging.info(f"User {from_number} requested daily digest.")
|
68 |
|
69 |
# Fetch the digest headlines
|
@@ -106,7 +149,7 @@ async def whatsapp_webhook_verify(request: Request):
|
|
106 |
if mode == "subscribe" and challenge:
|
107 |
logging.info(f"Webhook verification successful. Challenge: {challenge}")
|
108 |
# Challenge needs to be returned as an integer, not string
|
109 |
-
return JSONResponse(status_code=200, content=int(challenge))
|
110 |
else:
|
111 |
logging.warning(f"Webhook verification failed. Mode: {mode}, Challenge: {challenge}")
|
112 |
-
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Verification failed")
|
|
|
11 |
|
12 |
router = APIRouter()
|
13 |
|
14 |
+
def _extract_from_number_and_text(payload: dict):
|
15 |
+
"""
|
16 |
+
Extracts (from_number, message_text) from a WhatsApp webhook payload.
|
17 |
+
Supports text, button, and interactive replies. Silently ignores non-message webhooks.
|
18 |
+
Returns (None, None) if not a user message.
|
19 |
+
"""
|
20 |
+
try:
|
21 |
+
entries = payload.get("entry", [])
|
22 |
+
for entry in entries:
|
23 |
+
changes = entry.get("changes", [])
|
24 |
+
for change in changes:
|
25 |
+
# Only process message-type changes
|
26 |
+
if change.get("field") != "messages":
|
27 |
+
continue
|
28 |
+
|
29 |
+
value = change.get("value", {})
|
30 |
+
messages_list = value.get("messages", [])
|
31 |
+
if not messages_list:
|
32 |
+
# This may be a status/billing event without 'messages'
|
33 |
+
continue
|
34 |
+
|
35 |
+
# We only look at the first message in the list for this webhook
|
36 |
+
msg = messages_list[0]
|
37 |
+
from_number = msg.get("from")
|
38 |
+
mtype = msg.get("type")
|
39 |
+
|
40 |
+
# 1) Plain text
|
41 |
+
if mtype == "text":
|
42 |
+
text_body = (msg.get("text", {}) or {}).get("body")
|
43 |
+
if from_number and text_body:
|
44 |
+
return from_number, text_body
|
45 |
+
|
46 |
+
# 2) Template reply button (older/simple schema)
|
47 |
+
# payload is the stable key if you set it; fallback to text/title
|
48 |
+
if mtype == "button":
|
49 |
+
b = msg.get("button", {}) or {}
|
50 |
+
intent = b.get("payload") or b.get("text")
|
51 |
+
if from_number and intent:
|
52 |
+
return from_number, intent
|
53 |
+
|
54 |
+
# 3) Newer interactive replies (buttons or list)
|
55 |
+
if mtype == "interactive":
|
56 |
+
i = msg.get("interactive", {}) or {}
|
57 |
+
# Prefer stable IDs over display titles
|
58 |
+
if "button_reply" in i:
|
59 |
+
intent = i["button_reply"].get("id") or i["button_reply"].get("title")
|
60 |
+
if from_number and intent:
|
61 |
+
return from_number, intent
|
62 |
+
if "list_reply" in i:
|
63 |
+
intent = i["list_reply"].get("id") or i["list_reply"].get("title")
|
64 |
+
if from_number and intent:
|
65 |
+
return from_number, intent
|
66 |
+
|
67 |
+
# If we got here, no usable user message was found
|
68 |
+
return None, None
|
69 |
+
|
70 |
+
except Exception:
|
71 |
+
# In case of any unexpected structure, just treat as no user message
|
72 |
+
return None, None
|
73 |
+
|
74 |
+
|
75 |
# WhatsApp/Gupshup webhook endpoint
|
76 |
@router.post("/message-received")
|
77 |
async def whatsapp_webhook_receiver(request: Request):
|
|
|
91 |
logging.error("❌ Failed to decode webhook body as JSON")
|
92 |
return JSONResponse(status_code=400, content={"error": "Invalid JSON format"})
|
93 |
|
94 |
+
# <<< FIX 2: Robustly extract data from the nested structure (text/button/interactive) >>>
|
95 |
+
from_number, message_text = _extract_from_number_and_text(incoming_message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
|
97 |
# Check if sender and message text were successfully extracted
|
98 |
if not from_number or not message_text:
|
99 |
+
# This is likely a status/billing webhook or a non-user message; ignore quietly
|
100 |
+
logging.info("Ignoring non-message webhook or missing sender/text.")
|
101 |
+
return JSONResponse(status_code=200, content={"status": "ignored", "message": "No user message"})
|
102 |
|
103 |
logging.info(f"Message from {from_number}: {message_text}")
|
104 |
|
105 |
+
# Normalize intent text lightly (don't change your core logic)
|
106 |
+
normalized = message_text.lower().strip().replace("’", "'")
|
107 |
+
|
108 |
# Check for specific commands to send the digest
|
109 |
+
if normalized == "view today's headlines":
|
110 |
logging.info(f"User {from_number} requested daily digest.")
|
111 |
|
112 |
# Fetch the digest headlines
|
|
|
149 |
if mode == "subscribe" and challenge:
|
150 |
logging.info(f"Webhook verification successful. Challenge: {challenge}")
|
151 |
# Challenge needs to be returned as an integer, not string
|
152 |
+
return JSONResponse(status_code=200, content=int(challenge))
|
153 |
else:
|
154 |
logging.warning(f"Webhook verification failed. Mode: {mode}, Challenge: {challenge}")
|
155 |
+
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Verification failed")
|