webhook integration
Browse files- app.py +14 -14
- components/gateways/headlines_to_wa.py +7 -65
- routes/api/whatsapp_webhook.py +114 -0
app.py
CHANGED
@@ -1,34 +1,34 @@
|
|
1 |
-
# app.py
|
2 |
import os
|
3 |
import sys
|
4 |
from fastapi import FastAPI
|
|
|
|
|
5 |
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
6 |
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "components")))
|
7 |
-
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "routes")))
|
8 |
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "routes", "api")))
|
9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
-
#
|
12 |
-
from routes.api import ingest # Assuming routes/api/ingest.py exists and has a 'router'
|
13 |
-
from routes.api import query # Assuming routes/api/query.py exists and has a 'router'
|
14 |
-
from routes.api import headlines # Assuming routes/api/headlines.py exists and has a 'router'
|
15 |
-
from routes.api import wa_headlines
|
16 |
-
|
17 |
-
# You included Settings in your original, so I'll put it back.
|
18 |
-
# NOTE: Settings.llm = None can cause issues if LlamaIndex operations
|
19 |
-
# elsewhere expect a global LLM to be configured via Settings.
|
20 |
from llama_index.core.settings import Settings
|
21 |
Settings.llm = None
|
22 |
|
23 |
-
|
24 |
app = FastAPI()
|
25 |
|
26 |
@app.get("/")
|
27 |
def greet():
|
28 |
return {"welcome": "nuse ai"}
|
29 |
|
30 |
-
#
|
31 |
app.include_router(ingest.router)
|
32 |
app.include_router(query.router)
|
33 |
app.include_router(headlines.router)
|
34 |
-
app.include_router(wa_headlines.router)
|
|
|
|
|
|
1 |
import os
|
2 |
import sys
|
3 |
from fastapi import FastAPI
|
4 |
+
|
5 |
+
# Add paths to sys.path to allow relative imports
|
6 |
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
7 |
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "components")))
|
8 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "routes")))
|
9 |
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "routes", "api")))
|
10 |
|
11 |
+
# Import your API routes
|
12 |
+
from routes.api import ingest # routes/api/ingest.py
|
13 |
+
from routes.api import query # routes/api/query.py
|
14 |
+
from routes.api import headlines # routes/api/headlines.py
|
15 |
+
from routes.api import wa_headlines # routes/api/wa_headlines.py
|
16 |
+
from routes.api import whatsapp_webhook as whatsapp_webhook_router_module
|
17 |
|
18 |
+
# Optional: Global Settings (LlamaIndex)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
from llama_index.core.settings import Settings
|
20 |
Settings.llm = None
|
21 |
|
22 |
+
# Create FastAPI app
|
23 |
app = FastAPI()
|
24 |
|
25 |
@app.get("/")
|
26 |
def greet():
|
27 |
return {"welcome": "nuse ai"}
|
28 |
|
29 |
+
# Include your route modules
|
30 |
app.include_router(ingest.router)
|
31 |
app.include_router(query.router)
|
32 |
app.include_router(headlines.router)
|
33 |
+
app.include_router(wa_headlines.router)
|
34 |
+
app.include_router(whatsapp_webhook_router_module.router, prefix="/api/whatsapp", tags=["WhatsApp Webhook"])
|
components/gateways/headlines_to_wa.py
CHANGED
@@ -2,8 +2,8 @@ import os
|
|
2 |
import json
|
3 |
import redis
|
4 |
import requests
|
5 |
-
from fastapi import FastAPI
|
6 |
-
from fastapi.responses import JSONResponse
|
7 |
import logging
|
8 |
|
9 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
@@ -11,15 +11,12 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
|
|
11 |
# 🌐 Configuration from Environment Variables
|
12 |
# These variables MUST be set in your environment (e.g., .env file, shell exports, deployment configs)
|
13 |
REDIS_URL = os.environ.get("UPSTASH_REDIS_URL", "redis://localhost:6379")
|
14 |
-
# Reverting API URL to generic WhatsApp message endpoint
|
15 |
WHATSAPP_API_URL = os.environ.get("WHATSAPP_API_URL", "https://api.gupshup.io/wa/api/v1/msg")
|
16 |
WHATSAPP_TOKEN = os.environ.get("WHATSAPP_TOKEN")
|
17 |
WHATSAPP_TO_NUMBER = os.environ.get("WHATSAPP_TO_NUMBER", "353899495777") # e.g., "91xxxxxxxxxx"
|
18 |
GUPSHUP_SOURCE_NUMBER = os.environ.get("GUPSHUP_SOURCE_NUMBER") # e.g., your WABA number
|
19 |
GUPSHUP_APP_NAME = os.environ.get("GUPSHUP_APP_NAME") # e.g., your Gupshup app name
|
20 |
|
21 |
-
# Removed: WHATSAPP_CATALOG_ID, WHATSAPP_PRODUCT_RETAILER_ID, WHATSAPP_FOOTER_TEXT, WHATSAPP_TEMPLATE_ID
|
22 |
-
|
23 |
# ✅ Redis connection
|
24 |
try:
|
25 |
redis_client = redis.Redis.from_url(REDIS_URL, decode_responses=True)
|
@@ -30,7 +27,6 @@ except Exception as e:
|
|
30 |
raise
|
31 |
|
32 |
# 🧾 Fetch and format headlines
|
33 |
-
# Reverting this function to generate the full text message content
|
34 |
def fetch_cached_headlines() -> str:
|
35 |
try:
|
36 |
raw = redis_client.get("detailed_news_feed_cache")
|
@@ -64,7 +60,6 @@ def fetch_cached_headlines() -> str:
|
|
64 |
return "\n".join(message_parts)
|
65 |
|
66 |
# 📤 Send via Gupshup WhatsApp API
|
67 |
-
# Reverting to send a standard text message
|
68 |
def send_to_whatsapp(message_text: str) -> dict: # Function expects the full message text
|
69 |
# Validate critical environment variables for sending a message
|
70 |
if not WHATSAPP_TOKEN or \
|
@@ -81,7 +76,6 @@ def send_to_whatsapp(message_text: str) -> dict: # Function expects the full mes
|
|
81 |
"Cache-Control": "no-cache" # Add Cache-Control header
|
82 |
}
|
83 |
|
84 |
-
# <<< REVERTED MESSAGE PAYLOAD FOR STANDARD TEXT MESSAGE >>>
|
85 |
whatsapp_message_content = {
|
86 |
"type": "text",
|
87 |
"text": message_text # This is the full formatted text
|
@@ -112,61 +106,9 @@ def send_to_whatsapp(message_text: str) -> dict: # Function expects the full mes
|
|
112 |
logging.error(f"❌ An unexpected error occurred during WhatsApp send: {e}")
|
113 |
return {"status": "failed", "error": str(e), "code": 500}
|
114 |
|
115 |
-
#
|
116 |
-
|
117 |
-
|
118 |
-
@app.get("/send-daily-whatsapp")
|
119 |
-
def send_daily_whatsapp_digest():
|
120 |
-
logging.info("API Call: /send-daily-whatsapp initiated.")
|
121 |
-
|
122 |
-
# fetch_cached_headlines now returns the full message text
|
123 |
-
full_message_text = fetch_cached_headlines()
|
124 |
-
|
125 |
-
if full_message_text.startswith("❌") or full_message_text.startswith("⚠️"):
|
126 |
-
logging.warning(f"Returning error due to issue fetching headlines: {full_message_text}")
|
127 |
-
return JSONResponse(status_code=404, content={"error": full_message_text})
|
128 |
-
|
129 |
-
# Call send_to_whatsapp with the full formatted text
|
130 |
-
result = send_to_whatsapp(full_message_text)
|
131 |
-
|
132 |
-
if result.get("status") == "success":
|
133 |
-
logging.info("✅ WhatsApp message sent successfully.")
|
134 |
-
return JSONResponse(status_code=200, content=result)
|
135 |
-
else:
|
136 |
-
logging.error(f"❌ Failed to send WhatsApp message: {result.get('error')}")
|
137 |
-
return JSONResponse(status_code=result.get("code", 500), content=result)
|
138 |
|
139 |
-
#
|
140 |
-
|
141 |
-
# For local testing, ensure these environment variables are set in your shell or .env file.
|
142 |
-
# Example .env content:
|
143 |
-
# UPSTASH_REDIS_URL="redis://your_redis_url"
|
144 |
-
# WHATSAPP_API_URL="https://api.gupshup.io/wa/api/v1/msg"
|
145 |
-
# WHATSAPP_TOKEN="YOUR_GUPSHUP_API_KEY"
|
146 |
-
# WHATSAPP_TO_NUMBER="919999999999"
|
147 |
-
# GUPSHUP_SOURCE_NUMBER="15557926439"
|
148 |
-
# GUPSHUP_APP_NAME="NuseAI"
|
149 |
-
|
150 |
-
# Simulate a cached detailed feed for testing
|
151 |
-
dummy_cached_detailed_feed = {
|
152 |
-
"india": {
|
153 |
-
"1": {"title": "India's Economy Surges", "description": "Rapid growth in manufacturing and services sectors signals a strong economic recovery, boosting investor confidence and job creation.", "sources": ["Times of India"]},
|
154 |
-
"2": {"title": "New Tech Policy Unveiled", "description": "Government introduces new regulations to foster innovation while addressing data privacy concerns, aiming to balance growth with user protection.", "sources": ["Indian Express"]}
|
155 |
-
},
|
156 |
-
"world": {
|
157 |
-
"3": {"title": "Global Climate Talks Advance", "description": "Nations agree on ambitious new targets for emissions reduction, marking a significant step towards combating climate change despite earlier disagreements.", "sources": ["BBC News"]},
|
158 |
-
"4": {"title": "Space Mission Explores Mars", "description": "A new rover successfully lands on Mars, sending back groundbreaking data that could revolutionize our understanding of planetary geology and potential for life.", "sources": ["CNN World"]}
|
159 |
-
}
|
160 |
-
}
|
161 |
-
# Store dummy data in Redis for testing fetch_cached_headlines
|
162 |
-
redis_client.set("detailed_news_feed_cache", json.dumps(dummy_cached_detailed_feed))
|
163 |
-
|
164 |
-
|
165 |
-
logging.info("\n--- WhatsApp Message Preview ---\n")
|
166 |
-
msg_preview = fetch_cached_headlines()
|
167 |
-
print(msg_preview)
|
168 |
-
|
169 |
-
logging.info("\n--- Sending WhatsApp Message (Test) ---\n")
|
170 |
-
# This will attempt to send a real message if your env vars are valid
|
171 |
-
test_result = send_to_whatsapp(msg_preview)
|
172 |
-
print(test_result)
|
|
|
2 |
import json
|
3 |
import redis
|
4 |
import requests
|
5 |
+
from fastapi import FastAPI # This import is not strictly needed in this file if it's just a module
|
6 |
+
from fastapi.responses import JSONResponse # This import is not strictly needed in this file if it's just a module
|
7 |
import logging
|
8 |
|
9 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
11 |
# 🌐 Configuration from Environment Variables
|
12 |
# These variables MUST be set in your environment (e.g., .env file, shell exports, deployment configs)
|
13 |
REDIS_URL = os.environ.get("UPSTASH_REDIS_URL", "redis://localhost:6379")
|
|
|
14 |
WHATSAPP_API_URL = os.environ.get("WHATSAPP_API_URL", "https://api.gupshup.io/wa/api/v1/msg")
|
15 |
WHATSAPP_TOKEN = os.environ.get("WHATSAPP_TOKEN")
|
16 |
WHATSAPP_TO_NUMBER = os.environ.get("WHATSAPP_TO_NUMBER", "353899495777") # e.g., "91xxxxxxxxxx"
|
17 |
GUPSHUP_SOURCE_NUMBER = os.environ.get("GUPSHUP_SOURCE_NUMBER") # e.g., your WABA number
|
18 |
GUPSHUP_APP_NAME = os.environ.get("GUPSHUP_APP_NAME") # e.g., your Gupshup app name
|
19 |
|
|
|
|
|
20 |
# ✅ Redis connection
|
21 |
try:
|
22 |
redis_client = redis.Redis.from_url(REDIS_URL, decode_responses=True)
|
|
|
27 |
raise
|
28 |
|
29 |
# 🧾 Fetch and format headlines
|
|
|
30 |
def fetch_cached_headlines() -> str:
|
31 |
try:
|
32 |
raw = redis_client.get("detailed_news_feed_cache")
|
|
|
60 |
return "\n".join(message_parts)
|
61 |
|
62 |
# 📤 Send via Gupshup WhatsApp API
|
|
|
63 |
def send_to_whatsapp(message_text: str) -> dict: # Function expects the full message text
|
64 |
# Validate critical environment variables for sending a message
|
65 |
if not WHATSAPP_TOKEN or \
|
|
|
76 |
"Cache-Control": "no-cache" # Add Cache-Control header
|
77 |
}
|
78 |
|
|
|
79 |
whatsapp_message_content = {
|
80 |
"type": "text",
|
81 |
"text": message_text # This is the full formatted text
|
|
|
106 |
logging.error(f"❌ An unexpected error occurred during WhatsApp send: {e}")
|
107 |
return {"status": "failed", "error": str(e), "code": 500}
|
108 |
|
109 |
+
# Removed the FastAPI app instance and endpoint from here,
|
110 |
+
# as it will be defined in routes/api/wa_headlines.py
|
111 |
+
# and then included in app.py.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
|
113 |
+
# Removed the if __name__ == "__main__": block from here,
|
114 |
+
# as it will be in routes/api/wa_headlines.py for local testing.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
routes/api/whatsapp_webhook.py
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# routes/api/whatsapp_webhook.py
|
2 |
+
from fastapi import APIRouter, Request, HTTPException, status
|
3 |
+
from fastapi.responses import JSONResponse
|
4 |
+
import logging
|
5 |
+
import json
|
6 |
+
|
7 |
+
# Import your function to send messages back
|
8 |
+
from components.gateways.headlines_to_wa import fetch_cached_headlines, send_to_whatsapp
|
9 |
+
|
10 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
11 |
+
|
12 |
+
router = APIRouter()
|
13 |
+
|
14 |
+
# WhatsApp/Gupshup webhook endpoint
|
15 |
+
@router.post("/message-received")
|
16 |
+
async def whatsapp_webhook_receiver(request: Request):
|
17 |
+
"""
|
18 |
+
Receives incoming messages from Gupshup WhatsApp webhook.
|
19 |
+
Sends a daily news digest if the user sends a specific command.
|
20 |
+
"""
|
21 |
+
try:
|
22 |
+
# Gupshup sends data as application/x-www-form-urlencoded
|
23 |
+
# or sometimes as raw JSON depending on setup.
|
24 |
+
# We need to try parsing both.
|
25 |
+
try:
|
26 |
+
form_data = await request.form()
|
27 |
+
payload_str = form_data.get('payload') # Gupshup often wraps JSON in a 'payload' field
|
28 |
+
if payload_str:
|
29 |
+
incoming_message = json.loads(payload_str)
|
30 |
+
else: # If not 'payload' field, try direct form parsing
|
31 |
+
incoming_message = dict(form_data)
|
32 |
+
except json.JSONDecodeError:
|
33 |
+
# Fallback for raw JSON body (less common for Gupshup, but good to have)
|
34 |
+
incoming_message = await request.json()
|
35 |
+
except Exception as e:
|
36 |
+
logging.error(f"Error parsing webhook request body: {e}")
|
37 |
+
return JSONResponse(status_code=400, content={"status": "error", "message": "Invalid request format"})
|
38 |
+
|
39 |
+
|
40 |
+
logging.info(f"Received WhatsApp webhook: {json.dumps(incoming_message, indent=2)}")
|
41 |
+
|
42 |
+
# Extract relevant info (Gupshup webhook structure can vary, common fields used below)
|
43 |
+
# This part might need fine-tuning based on actual Gupshup webhook JSON
|
44 |
+
message_data = incoming_message.get('payload', {}).get('payload', {}) # Gupshup often double-nests 'payload'
|
45 |
+
|
46 |
+
# Try a different path if the above didn't work (common for raw JSON webhooks)
|
47 |
+
if not message_data:
|
48 |
+
message_data = incoming_message.get('message', {})
|
49 |
+
if not message_data: # Sometimes the direct message object is at the top level
|
50 |
+
message_data = incoming_message
|
51 |
+
|
52 |
+
from_number = message_data.get('sender', {}).get('phone') or message_data.get('from')
|
53 |
+
message_text = message_data.get('message', {}).get('text') or message_data.get('body') # Common text fields
|
54 |
+
|
55 |
+
if not from_number or not message_text:
|
56 |
+
logging.warning("Received webhook without valid sender or message text.")
|
57 |
+
return JSONResponse(status_code=200, content={"status": "ignored", "message": "Missing sender or text"})
|
58 |
+
|
59 |
+
logging.info(f"Message from {from_number}: {message_text}")
|
60 |
+
|
61 |
+
# Check for specific commands to send the digest
|
62 |
+
if message_text.lower().strip() == "digest":
|
63 |
+
logging.info(f"User {from_number} requested daily digest.")
|
64 |
+
|
65 |
+
# Fetch the digest headlines
|
66 |
+
full_message_text = fetch_cached_headlines()
|
67 |
+
|
68 |
+
if full_message_text.startswith("❌") or full_message_text.startswith("⚠️"):
|
69 |
+
logging.error(f"Failed to fetch digest for {from_number}: {full_message_text}")
|
70 |
+
# Send an error message back to the user
|
71 |
+
send_to_whatsapp(f"Sorry, I couldn't fetch the news digest today. {full_message_text}", destination_number=from_number)
|
72 |
+
return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to fetch digest"})
|
73 |
+
|
74 |
+
# Send the digest back to the user who requested it
|
75 |
+
result = send_to_whatsapp(full_message_text, destination_number=from_number)
|
76 |
+
|
77 |
+
if result.get("status") == "success":
|
78 |
+
logging.info(f"✅ Successfully sent digest to {from_number}.")
|
79 |
+
return JSONResponse(status_code=200, content={"status": "success", "message": "Digest sent"})
|
80 |
+
else:
|
81 |
+
logging.error(f"❌ Failed to send digest to {from_number}: {result.get('error')}")
|
82 |
+
# Send an error message back to the user
|
83 |
+
send_to_whatsapp(f"Sorry, I couldn't send the news digest to you. Error: {result.get('error', 'unknown')}", destination_number=from_number)
|
84 |
+
return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to send digest"})
|
85 |
+
else:
|
86 |
+
logging.info(f"Received unhandled message from {from_number}: '{message_text}'")
|
87 |
+
# Optional: Send a generic response for unhandled commands
|
88 |
+
# send_to_whatsapp("Sorry, I only understand 'Digest' for now.", destination_number=from_number)
|
89 |
+
return JSONResponse(status_code=200, content={"status": "ignored", "message": "No action taken for this command"})
|
90 |
+
|
91 |
+
except Exception as e:
|
92 |
+
logging.error(f"Error processing webhook: {e}", exc_info=True)
|
93 |
+
return JSONResponse(status_code=500, content={"status": "error", "message": str(e)})
|
94 |
+
|
95 |
+
# Gupshup webhook verification endpoint (GET request with 'hub.mode' and 'hub.challenge')
|
96 |
+
@router.get("/message-received")
|
97 |
+
async def whatsapp_webhook_verify(request: Request):
|
98 |
+
"""
|
99 |
+
Endpoint for Gupshup webhook verification.
|
100 |
+
"""
|
101 |
+
mode = request.query_params.get("hub.mode")
|
102 |
+
challenge = request.query_params.get("hub.challenge")
|
103 |
+
verify_token = request.query_params.get("hub.verify_token") # You might set an env var for this
|
104 |
+
|
105 |
+
# Gupshup typically doesn't require a verify_token, unlike Facebook directly.
|
106 |
+
# However, if you configure it, you should check it.
|
107 |
+
# For now, we'll just return the challenge if mode is 'subscribe'.
|
108 |
+
|
109 |
+
if mode == "subscribe" and challenge:
|
110 |
+
logging.info(f"Webhook verification successful. Challenge: {challenge}")
|
111 |
+
return JSONResponse(status_code=200, content=int(challenge)) # Challenge needs to be an integer
|
112 |
+
else:
|
113 |
+
logging.warning(f"Webhook verification failed. Mode: {mode}, Challenge: {challenge}")
|
114 |
+
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Verification failed")
|