Spaces:
Sleeping
Sleeping
| import os, json | |
| from fastapi import FastAPI, Request, HTTPException, Query | |
| from fastapi.responses import PlainTextResponse, JSONResponse | |
| import httpx | |
| import base64 | |
| import asyncio | |
| app = FastAPI() | |
| VERIFY_TOKEN = os.getenv("VERIFY_TOKEN", "") # set in HF Secrets | |
| PHONE_NUMBER_ID = os.getenv("PHONE_NUMBER_ID", "") | |
| WHATSAPP_API_TOKEN = os.getenv("WHATSAPP_API_TOKEN", "") | |
| CLIENT_API_URL = os.getenv("CLIENT_API_URL", "") | |
| # Add task tracking at the top | |
| active_tasks = {} | |
| # Persistent deduplication using a file | |
| DEDUP_FILE = "/tmp/processed_message_ids.txt" | |
| def load_processed_ids(): | |
| try: | |
| with open(DEDUP_FILE, "r") as f: | |
| return set(line.strip() for line in f if line.strip()) | |
| except FileNotFoundError: | |
| return set() | |
| def save_processed_id(msg_id): | |
| try: | |
| with open(DEDUP_FILE, "a") as f: | |
| f.write(msg_id + "\n") | |
| except Exception as e: | |
| print(f"Error saving msg_id {msg_id}: {e}") | |
| processed_message_ids = load_processed_ids() | |
| async def forward_to_client(b64_str: str, wa_id: str, task_id: str): | |
| """Fire-and-forget call to CLIENT /api/process_id.""" | |
| payload = { | |
| "data": [b64_str], | |
| "configurations": { | |
| "iddetectionconfidence": "80", | |
| "smartidconfidence": "50", | |
| "greenidconfidence": "50", | |
| "binaryString": "1111111011110011" | |
| } | |
| } | |
| short = b64_str[:40] # first 40 chars for reference | |
| try: | |
| print(f"[CLIENT] ➜ sending image ({len(b64_str)} bytes) " | |
| f"preview='{short}…'") | |
| print("Payload to CLIENT:", json.dumps(payload)[:300], "…") | |
| async with httpx.AsyncClient(timeout=60) as client: | |
| resp = await client.post(CLIENT_API_URL, json=payload) | |
| resp.raise_for_status() | |
| # ── log the full JSON response (truncated to 300 chars for brevity) ── | |
| data = resp.json() | |
| print(f"[CLIENT] ✔ 200 {json.dumps(data)[:300]}…") | |
| body = data | |
| print(f"[CLIENT] ✔ {resp.status_code}") | |
| pretty_print_dict(body) | |
| # ── send parsed ID back to WhatsApp or error message ── | |
| if wa_id and task_id in active_tasks: | |
| if data.get("status") == "success": | |
| print(f"[TASK] Sending response for active task {task_id}") | |
| det = data["results"][0]["detections"][0] | |
| reply = ( | |
| "✅ *ID processed*\n" | |
| f"*Name*: {det.get('name','?')} {det.get('surname','')}\n" | |
| f"*ID No.*: {det.get('id_num','?')}\n" | |
| f"*DOB*: {det.get('date_of_birth','?')}\n" | |
| f"*Gender*: {det.get('gender','?')}\n" | |
| f"*ID Type*: {det.get('id_type','?')}\n" | |
| f"*Country of Birth*: {det.get('cob','?')}\n" | |
| f"*Citizen*: {det.get('citizen','?')}\n" | |
| f"*Citizen Status*: {det.get('citizen_status','?')}\n" | |
| f"*Date Issued*: {det.get('date_issued','?')}\n" | |
| f"*Age*: {det.get('age','?')}" | |
| ) | |
| send_whatsapp_message(wa_id, reply) | |
| print(f"[TASK] Completed and removed task {task_id}") | |
| elif "error" in data: | |
| error_msg = data["error"] | |
| print(f"[TASK] CLIENT error for task {task_id}: {error_msg}") | |
| send_whatsapp_message(wa_id, f"❌ {error_msg}") | |
| else: | |
| print(f"[TASK] CLIENT response not successful for task {task_id}") | |
| send_whatsapp_message(wa_id, "❌ Sorry, we could not process your ID. Please try again with a clearer image.") | |
| # Remove task from active list | |
| active_tasks.pop(task_id, None) | |
| elif task_id not in active_tasks: | |
| print(f"[TASK] Ignoring response for inactive task {task_id}") | |
| except Exception as e: | |
| print(f"[CLIENT] ✘ error while sending '{short}…' →", e) | |
| # Remove task from active list on error | |
| active_tasks.pop(task_id, None) | |
| def pretty_print_dict(d, indent=0): | |
| for k, v in d.items(): | |
| if isinstance(v, dict): | |
| print(' ' * indent + f"{k}:") | |
| pretty_print_dict(v, indent + 2) | |
| elif isinstance(v, list): | |
| print(' ' * indent + f"{k}: [") | |
| for item in v: | |
| if isinstance(item, dict): | |
| pretty_print_dict(item, indent + 2) | |
| else: | |
| print(' ' * (indent + 2) + str(item)) | |
| print(' ' * indent + "]") | |
| else: | |
| print(' ' * indent + f"{k}: {v}") | |
| # verification | |
| def verify( | |
| hub_mode: str = Query(None, alias="hub.mode"), | |
| hub_token: str = Query(None, alias="hub.verify_token"), | |
| hub_challenge: str = Query(None, alias="hub.challenge") | |
| ): | |
| if hub_mode == "subscribe" and hub_token == VERIFY_TOKEN: | |
| return PlainTextResponse(hub_challenge) | |
| raise HTTPException(status_code=403, detail="Invalid token") | |
| def send_whatsapp_message(wa_id, reply): | |
| url = f"https://graph.facebook.com/v23.0/{PHONE_NUMBER_ID}/messages" | |
| headers = { | |
| "Authorization": f"Bearer {WHATSAPP_API_TOKEN}", | |
| "Content-Type": "application/json" | |
| } | |
| payload = { | |
| "messaging_product": "whatsapp", | |
| "to": wa_id, | |
| "type": "text", | |
| "text": {"body": reply} | |
| } | |
| httpx.post(url, headers=headers, json=payload) | |
| def send_welcome_buttons(wa_id: str): | |
| """Send an in-session Interactive button message with 2 reply options.""" | |
| url = f"https://graph.facebook.com/v23.0/{PHONE_NUMBER_ID}/messages" | |
| headers = { | |
| "Authorization": f"Bearer {WHATSAPP_API_TOKEN}", | |
| "Content-Type": "application/json", | |
| } | |
| payload = { | |
| "messaging_product": "whatsapp", | |
| "to": wa_id, | |
| "type": "interactive", | |
| "interactive": { | |
| "type": "button", | |
| "body": { | |
| "text": "Welcome to CLIENT.\nPlease send an image of your South African ID document." | |
| }, | |
| "action": { | |
| "buttons": [ | |
| { | |
| "type": "reply", | |
| "reply": {"id": "open_gallery", "title": "📎"} | |
| }, | |
| { | |
| "type": "reply", | |
| "reply": {"id": "open_camera", "title": "📷"} | |
| } | |
| ] | |
| } | |
| } | |
| } | |
| resp = httpx.post(url, headers=headers, json=payload) | |
| # debug log | |
| print(f"[BUTTON] POST → {resp.status_code}, body: {resp.text}") | |
| # incoming WhatsApp updates | |
| async def receive_update(req: Request): | |
| data = await req.json() | |
| try: | |
| change = data["entry"][0]["changes"][0]["value"] | |
| contacts = change.get("contacts") | |
| messages = change.get("messages") | |
| if not contacts or not messages: | |
| # Missing expected payload—ignore | |
| print("Ignoring update, no contacts or messages field") | |
| return JSONResponse({"status": "ignored", "reason": "no contacts/messages"}) | |
| wa_id = contacts[0]["wa_id"] | |
| msg = messages[0] | |
| except (KeyError, IndexError, TypeError) as e: | |
| print("Malformed webhook payload:", e) | |
| return JSONResponse({"status": "ignored", "reason": "malformed payload"}) | |
| msg_id = msg.get("id") | |
| if msg_id in processed_message_ids: | |
| print(f"Duplicate message {msg_id}, skipping.") | |
| return JSONResponse({"status": "duplicate", "id": msg_id}) | |
| processed_message_ids.add(msg_id) | |
| save_processed_id(msg_id) # Save to file | |
| # TEXT: send welcome message | |
| if msg.get("type") == "text": | |
| send_whatsapp_message( | |
| wa_id, | |
| "Welcome to CLIENT! 📋\n\n" | |
| "Please send an image of your South African ID document for processing." | |
| ) | |
| return JSONResponse({"status": "welcome_sent"}) | |
| # IMAGE: if user sent an image, convert to Base64 preview | |
| if msg["type"] == "image": | |
| media_id = msg["image"]["id"] | |
| # 1) get media URL | |
| media_resp = httpx.get( | |
| f"https://graph.facebook.com/v23.0/{media_id}", | |
| headers={"Authorization": f"Bearer {WHATSAPP_API_TOKEN}"} | |
| ) | |
| media_resp.raise_for_status() | |
| media_url = media_resp.json()["url"] | |
| # 2) download bytes with auth | |
| media_bytes = httpx.get( | |
| media_url, | |
| headers={"Authorization": f"Bearer {WHATSAPP_API_TOKEN}"} | |
| ).content | |
| # 3) encode full & preview | |
| full_b64 = base64.b64encode(media_bytes).decode("utf-8") | |
| preview = full_b64[:10] | |
| # 4) forward full string to CLIENT (non-blocking) | |
| task_id = str(msg_id) # Use msg_id as task_id | |
| active_tasks[task_id] = True # Add task to active list | |
| print(f"[TASK] Created task {task_id} for message {msg_id}") | |
| asyncio.create_task(forward_to_client(full_b64, wa_id, task_id)) | |
| # 5) echo preview to the user | |
| return JSONResponse({"status": "image_preview_sent", "preview": preview}) | |
| # OTHER: ignore | |
| return JSONResponse({"status": "ignored", "type": msg["type"]}) | |
| # health-check endpoint for token | |
| async def check_token(): | |
| token = os.getenv("WHATSAPP_API_TOKEN", "") | |
| if token: | |
| return {"token_loaded": True} | |
| raise HTTPException(status_code=400, detail="Token not found") | |
| # health check | |
| def ping(): | |
| return {"pong": True} |