ragV98 commited on
Commit
3566f32
·
1 Parent(s): a330e1e

webhook integration

Browse files
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
- # --- Your original imports ---
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
- # --- Your original router inclusions ---
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
- # 🚀 FastAPI App
116
- app = FastAPI()
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
- # 🧪 For local testing
140
- if __name__ == "__main__":
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")