Acytel commited on
Commit
c69fb74
Β·
1 Parent(s): 8d003ac

Fix ghost variables and timeout

Browse files
Files changed (1) hide show
  1. whatsapp/webhook.py +62 -34
whatsapp/webhook.py CHANGED
@@ -3,28 +3,23 @@ import hashlib
3
  import os
4
  import httpx
5
  import datetime
 
6
  from fastapi import APIRouter, Request, HTTPException, BackgroundTasks
7
  from fastapi.responses import PlainTextResponse
8
  from supabase import create_client
9
 
10
  router = APIRouter()
11
 
12
- VERIFY_TOKEN = os.environ.get("WHATSAPP_VERIFY_TOKEN")
13
- WA_TOKEN = os.environ.get("WHATSAPP_TOKEN")
14
- PHONE_NUMBER_ID = os.environ.get("WHATSAPP_PHONE_NUMBER_ID")
15
- APP_SECRET = os.environ.get("WHATSAPP_APP_SECRET") # Fixed signature secret
16
-
17
- SUPABASE_URL = os.environ.get("SUPABASE_URL")
18
- SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
19
- supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
20
-
21
- WA_API_URL = f"https://graph.facebook.com/v19.0/{PHONE_NUMBER_ID}/messages"
22
 
23
  # ── Webhook Verification (GET) ────────────────────────────────────────────────
24
  @router.get("/webhook/whatsapp")
25
  async def verify_webhook(request: Request):
26
  params = request.query_params
27
- if params.get("hub.mode") == "subscribe" and params.get("hub.verify_token") == VERIFY_TOKEN:
28
  print("βœ… WhatsApp webhook verified")
29
  return PlainTextResponse(content=params.get("hub.challenge"))
30
  raise HTTPException(status_code=403, detail="Verification failed")
@@ -32,11 +27,11 @@ async def verify_webhook(request: Request):
32
  # ── Incoming Message Handler (POST) ──────────────────────────────────────────
33
  @router.post("/webhook/whatsapp")
34
  async def receive_message(request: Request, background_tasks: BackgroundTasks):
35
- # Verify HMAC signature using APP_SECRET
36
  signature = request.headers.get("X-Hub-Signature-256", "")
37
  body = await request.body()
 
38
 
39
- expected = "sha256=" + hmac.new(APP_SECRET.encode(), body, hashlib.sha256).hexdigest()
40
  if not hmac.compare_digest(signature, expected):
41
  raise HTTPException(status_code=403, detail="Invalid signature")
42
 
@@ -44,7 +39,7 @@ async def receive_message(request: Request, background_tasks: BackgroundTasks):
44
  try:
45
  entry = data["entry"][0]["changes"][0]["value"]
46
  if "messages" not in entry:
47
- return {"status": "ok"} # Ignore status updates (read receipts, etc.)
48
 
49
  message = entry["messages"][0]
50
  from_number = message["from"]
@@ -62,27 +57,61 @@ async def receive_message(request: Request, background_tasks: BackgroundTasks):
62
 
63
  return {"status": "ok"}
64
 
 
65
  async def send_whatsapp_message(to: str, text: str):
66
- """Send a text reply via WhatsApp Cloud API."""
67
  if len(text) > 4096:
68
  text = text[:4000] + "\n\n_[Reply truncated. Visit govbridge.in for full answer]_"
69
 
 
 
 
 
 
 
 
 
 
 
 
70
  payload = {
71
  "messaging_product": "whatsapp",
72
  "to": to,
73
  "type": "text",
74
  "text": {"body": text}
75
  }
76
- async with httpx.AsyncClient() as client:
77
- await client.post(
78
- WA_API_URL, json=payload, headers={"Authorization": f"Bearer {WA_TOKEN}"}
79
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
  def check_and_increment_quota() -> bool:
82
- """Returns True if within quota, False if limit reached."""
83
  current_month = datetime.datetime.utcnow().strftime("%Y-%m")
 
 
84
 
85
- # Supabase atomic upsert via RPC or standard select/update
 
 
 
 
86
  res = supabase.table("whatsapp_quota").select("conversation_count").eq("month", current_month).execute()
87
 
88
  if res.data:
@@ -96,16 +125,10 @@ def check_and_increment_quota() -> bool:
96
  return True
97
 
98
  async def process_and_reply(from_number: str, query: str, language: str):
99
- """Full RAG pipeline for WhatsApp."""
100
  if not check_and_increment_quota():
101
  await send_whatsapp_message(from_number, "⚠️ Our WhatsApp service has reached its monthly limit. Please visit govbridge.in for full access.")
102
  return
103
 
104
- # TODO: Import your actual run_rag_pipeline function here once refactored
105
- # from api import run_rag_pipeline
106
- # answer, sources = await run_rag_pipeline(query, language)
107
-
108
- # Mocking response until Step 4 refactor is complete
109
  answer = f"Echoing your RAG query: {query}"
110
  sources = ["Source 1", "Source 2"]
111
 
@@ -117,20 +140,25 @@ async def process_and_reply(from_number: str, query: str, language: str):
117
  await send_whatsapp_message(from_number, reply)
118
 
119
  async def process_voice_and_reply(from_number: str, audio_id: str):
120
- """Download voice message and transcribe via Whisper."""
 
 
121
  async with httpx.AsyncClient() as client:
122
  meta_resp = await client.get(
123
- f"https://graph.facebook.com/v19.0/{audio_id}",
124
- headers={"Authorization": f"Bearer {WA_TOKEN}"}
125
  )
 
 
 
 
126
  audio_url = meta_resp.json()["url"]
127
-
128
- audio_resp = await client.get(audio_url, headers={"Authorization": f"Bearer {WA_TOKEN}"})
129
  audio_bytes = audio_resp.content
130
 
131
  transcription_resp = await client.post(
132
  "https://api.groq.com/openai/v1/audio/transcriptions",
133
- headers={"Authorization": f"Bearer {os.environ['GROQ_API_KEY']}"},
134
  files={"file": ("audio.ogg", audio_bytes, "audio/ogg")},
135
  data={"model": "whisper-large-v3-turbo", "language": "hi"}
136
  )
@@ -139,4 +167,4 @@ async def process_voice_and_reply(from_number: str, audio_id: str):
139
  if transcript:
140
  await process_and_reply(from_number, transcript, "hindi")
141
  else:
142
- await send_whatsapp_message(from_number, "πŸ™ Sorry, I couldn't understand the voice message. Please type your question.")
 
3
  import os
4
  import httpx
5
  import datetime
6
+ import asyncio
7
  from fastapi import APIRouter, Request, HTTPException, BackgroundTasks
8
  from fastapi.responses import PlainTextResponse
9
  from supabase import create_client
10
 
11
  router = APIRouter()
12
 
13
+ # ── Dynamic Secret Loader ────────────────────────────────────────────────────
14
+ def get_secret(key: str) -> str:
15
+ """Dynamically fetches secrets to prevent Hugging Face soft-reboot caching."""
16
+ return os.environ.get(key, "").strip()
 
 
 
 
 
 
17
 
18
  # ── Webhook Verification (GET) ────────────────────────────────────────────────
19
  @router.get("/webhook/whatsapp")
20
  async def verify_webhook(request: Request):
21
  params = request.query_params
22
+ if params.get("hub.mode") == "subscribe" and params.get("hub.verify_token") == get_secret("WHATSAPP_VERIFY_TOKEN"):
23
  print("βœ… WhatsApp webhook verified")
24
  return PlainTextResponse(content=params.get("hub.challenge"))
25
  raise HTTPException(status_code=403, detail="Verification failed")
 
27
  # ── Incoming Message Handler (POST) ──────────────────────────────────────────
28
  @router.post("/webhook/whatsapp")
29
  async def receive_message(request: Request, background_tasks: BackgroundTasks):
 
30
  signature = request.headers.get("X-Hub-Signature-256", "")
31
  body = await request.body()
32
+ app_secret = get_secret("WHATSAPP_APP_SECRET")
33
 
34
+ expected = "sha256=" + hmac.new(app_secret.encode(), body, hashlib.sha256).hexdigest()
35
  if not hmac.compare_digest(signature, expected):
36
  raise HTTPException(status_code=403, detail="Invalid signature")
37
 
 
39
  try:
40
  entry = data["entry"][0]["changes"][0]["value"]
41
  if "messages" not in entry:
42
+ return {"status": "ok"}
43
 
44
  message = entry["messages"][0]
45
  from_number = message["from"]
 
57
 
58
  return {"status": "ok"}
59
 
60
+ # ── Outgoing Message Pipeline ────────────────────────────────────────────────
61
  async def send_whatsapp_message(to: str, text: str):
 
62
  if len(text) > 4096:
63
  text = text[:4000] + "\n\n_[Reply truncated. Visit govbridge.in for full answer]_"
64
 
65
+ clean_phone_id = get_secret("WHATSAPP_PHONE_NUMBER_ID")
66
+ clean_token = get_secret("WHATSAPP_TOKEN")
67
+
68
+ print(f"\nπŸš€ Initiating reply to {to}...")
69
+ print(f"πŸ”‘ Keys Loaded -> Phone ID: '{clean_phone_id}' | Token Length: {len(clean_token)}")
70
+
71
+ if not clean_phone_id or not clean_token:
72
+ print("🚨 FATAL ERROR: API Keys are completely empty. Hugging Face failed to load secrets!")
73
+ return
74
+
75
+ api_url = f"https://graph.facebook.com/v25.0/{clean_phone_id}/messages"
76
  payload = {
77
  "messaging_product": "whatsapp",
78
  "to": to,
79
  "type": "text",
80
  "text": {"body": text}
81
  }
82
+ headers = {
83
+ "Authorization": f"Bearer {clean_token}",
84
+ "Content-Type": "application/json"
85
+ }
86
+
87
+ try:
88
+ # Primary Attempt: Fast Asynchronous Request
89
+ async with httpx.AsyncClient(timeout=15.0) as client:
90
+ resp = await client.post(api_url, json=payload, headers=headers)
91
+ print(f"πŸ“€ Meta Send Response (ASYNC): {resp.status_code} - {resp.text}")
92
+ except httpx.ConnectTimeout:
93
+ print("⚠️ ConnectTimeout triggered! Falling back to synchronous network request...")
94
+ # Backup Attempt: Bypasses async IPv6 network glitches
95
+ try:
96
+ resp = await asyncio.to_thread(
97
+ lambda: httpx.Client().post(api_url, json=payload, headers=headers, timeout=15.0)
98
+ )
99
+ print(f"πŸ“€ Meta Send Response (SYNC FALLBACK): {resp.status_code} - {resp.text}")
100
+ except Exception as fallback_e:
101
+ print(f"❌ Fallback completely failed: {fallback_e}")
102
+ except Exception as e:
103
+ print(f"❌ Unknown send error: {e}")
104
 
105
  def check_and_increment_quota() -> bool:
 
106
  current_month = datetime.datetime.utcnow().strftime("%Y-%m")
107
+ supabase_url = get_secret("SUPABASE_URL")
108
+ supabase_key = get_secret("SUPABASE_KEY")
109
 
110
+ if not supabase_url or not supabase_key:
111
+ print("⚠️ Supabase keys missing, skipping quota check.")
112
+ return True
113
+
114
+ supabase = create_client(supabase_url, supabase_key)
115
  res = supabase.table("whatsapp_quota").select("conversation_count").eq("month", current_month).execute()
116
 
117
  if res.data:
 
125
  return True
126
 
127
  async def process_and_reply(from_number: str, query: str, language: str):
 
128
  if not check_and_increment_quota():
129
  await send_whatsapp_message(from_number, "⚠️ Our WhatsApp service has reached its monthly limit. Please visit govbridge.in for full access.")
130
  return
131
 
 
 
 
 
 
132
  answer = f"Echoing your RAG query: {query}"
133
  sources = ["Source 1", "Source 2"]
134
 
 
140
  await send_whatsapp_message(from_number, reply)
141
 
142
  async def process_voice_and_reply(from_number: str, audio_id: str):
143
+ clean_token = get_secret("WHATSAPP_TOKEN")
144
+ groq_key = get_secret("GROQ_API_KEY")
145
+
146
  async with httpx.AsyncClient() as client:
147
  meta_resp = await client.get(
148
+ f"https://graph.facebook.com/v25.0/{audio_id}",
149
+ headers={"Authorization": f"Bearer {clean_token}"}
150
  )
151
+ if meta_resp.status_code != 200:
152
+ print(f"❌ Failed to get audio metadata: {meta_resp.text}")
153
+ return
154
+
155
  audio_url = meta_resp.json()["url"]
156
+ audio_resp = await client.get(audio_url, headers={"Authorization": f"Bearer {clean_token}"})
 
157
  audio_bytes = audio_resp.content
158
 
159
  transcription_resp = await client.post(
160
  "https://api.groq.com/openai/v1/audio/transcriptions",
161
+ headers={"Authorization": f"Bearer {groq_key}"},
162
  files={"file": ("audio.ogg", audio_bytes, "audio/ogg")},
163
  data={"model": "whisper-large-v3-turbo", "language": "hi"}
164
  )
 
167
  if transcript:
168
  await process_and_reply(from_number, transcript, "hindi")
169
  else:
170
+ await send_whatsapp_message(from_number, "πŸ™ Sorry, I couldn't understand the voice message. Please type your question.")