WhatsappChatBot / main.py
Zenaight's picture
Upload main.py
ccab90f verified
Raw
History Blame Contribute Delete
9.61 kB
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}")
@app.get("/webhook") # 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}")
@app.post("/webhook") # 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
@app.post("/send")
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")
@app.get("/ping") # health check
def ping():
return {"pong": True}