| import json |
| from fastapi import FastAPI, Request |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.responses import JSONResponse |
|
|
| from greenapi import ( |
| extract_text_message, |
| get_device_key_by_instance_id, |
| send_text_message, |
| send_url_button, |
| ) |
|
|
| from engine.conversation_engine import process_message |
| from messages import log_message |
|
|
| from handoff.sales import create_sales_handoff |
| from handoff.support import create_support_handoff |
|
|
| app = FastAPI() |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=False, |
| allow_methods=["POST", "OPTIONS"], |
| allow_headers=["*"], |
| ) |
|
|
|
|
| @app.get("/health") |
| async def health(): |
| print(">>> /health called") |
| return {"ok": True, "service": "up"} |
|
|
|
|
| @app.get("/") |
| async def root(): |
| return { |
| "ok": True, |
| "service": "ADK Bot API", |
| "message": "Server is running" |
| } |
|
|
|
|
| @app.get("/receive-test") |
| async def receive_test(): |
| print(">>> /receive-test called") |
| return {"ok": True, "message": "receive endpoint reachable"} |
|
|
|
|
| @app.post("/receive") |
| async def webhook_receiver(req: Request): |
| print("========== /receive HIT ==========") |
|
|
| try: |
| raw_body = await req.body() |
| print(">>> RAW BODY BYTES:") |
| print(raw_body) |
| except Exception as e: |
| print(">>> FAILED TO READ RAW BODY:", str(e)) |
| raw_body = b"" |
|
|
| try: |
| headers = dict(req.headers) |
| print(">>> HEADERS:") |
| print(json.dumps(headers, ensure_ascii=False, indent=2)) |
| except Exception as e: |
| print(">>> FAILED TO READ HEADERS:", str(e)) |
|
|
| body = None |
|
|
| try: |
| if raw_body: |
| body = json.loads(raw_body.decode("utf-8")) |
| else: |
| body = {} |
| except Exception as e: |
| print(">>> FAILED TO PARSE JSON:", str(e)) |
| return JSONResponse( |
| status_code=200, |
| content={ |
| "ok": True, |
| "debug": "request reached /receive but body is not valid JSON" |
| } |
| ) |
|
|
| print("===== RECEIVED JSON =====") |
| print(json.dumps(body, ensure_ascii=False, indent=2)) |
| print("=========================") |
|
|
| |
| webhook_type = body.get("typeWebhook") |
| print(">>> typeWebhook:", webhook_type) |
|
|
| if webhook_type != "incomingMessageReceived": |
| print(">>> ignored non-incoming webhook") |
| return { |
| "ok": True, |
| "ignored": True, |
| "typeWebhook": webhook_type |
| } |
|
|
| instance_data = body.get("instanceData") or {} |
| sender_data = body.get("senderData") or {} |
| msg_data = body.get("messageData") or {} |
|
|
| |
| print(">>> instanceData:", instance_data) |
| print(">>> senderData:", sender_data) |
| print(">>> messageData:", msg_data) |
|
|
| |
| instance_id = str( |
| instance_data.get("idInstance") |
| or instance_data.get("wid") |
| or "" |
| ) |
|
|
| chat_id = sender_data.get("chatId") |
| text_message = extract_text_message(msg_data) |
|
|
| print(">>> resolved instance_id:", instance_id) |
| print(">>> resolved chat_id:", chat_id) |
| print(">>> resolved text_message:", text_message) |
|
|
| if not instance_id or not chat_id or not text_message: |
| print(">>> skipped because missing required fields") |
| return { |
| "ok": True, |
| "skipped": True, |
| "resolved_instance_id": instance_id, |
| "resolved_chat_id": chat_id, |
| "resolved_text_message": text_message, |
| } |
|
|
| device_key = get_device_key_by_instance_id(instance_id) |
| print(">>> resolved device_key:", device_key) |
|
|
| if not device_key: |
| print(">>> Unknown instance_id:", instance_id) |
| return { |
| "ok": True, |
| "skipped": True, |
| "reason": "unknown_instance_id", |
| "instance_id": instance_id |
| } |
|
|
| customer_phone = chat_id.replace("@c.us", "") |
| bot_number = device_key |
|
|
| try: |
| log_message(customer_phone, bot_number, "in", text_message) |
| except Exception as e: |
| print(">>> failed to log incoming message:", str(e)) |
|
|
| try: |
| result = process_message( |
| bot_number=bot_number, |
| customer_phone=customer_phone, |
| text=text_message |
| ) |
| print(">>> engine result:") |
| print(json.dumps(result, ensure_ascii=False, indent=2)) |
| except Exception as e: |
| print(">>> engine error:", str(e)) |
|
|
| fallback_reply = "تمام، حصلت مشكلة بسيطة. ممكن تعيد إرسال رسالتك مرة تانية؟" |
| try: |
| status, txt = send_text_message(device_key, chat_id, fallback_reply) |
| print(">>> fallback send status:", status) |
| print(">>> fallback send body:", txt) |
| log_message(customer_phone, bot_number, "out", fallback_reply) |
| except Exception as send_err: |
| print(">>> failed to send fallback:", str(send_err)) |
|
|
| return JSONResponse( |
| status_code=200, |
| content={"ok": True, "error": str(e), "fallback_sent": True} |
| ) |
|
|
| reply = (result.get("reply") or "").strip() |
| action = result.get("action") |
|
|
| if action and action.get("type") == "url_button": |
| try: |
| status, txt = send_url_button( |
| device_key=device_key, |
| chat_id=chat_id, |
| header=action.get("header", "مرحباً بك"), |
| body=action.get("body", reply), |
| footer=action.get("footer", ""), |
| url=action.get("url"), |
| button_text=action.get("button_text", "فتح الرابط") |
| ) |
| print(">>> sent url button status:", status) |
| print(">>> sent url button body:", txt) |
|
|
| try: |
| log_message(customer_phone, bot_number, "out", action.get("body", reply)) |
| except Exception as e: |
| print(">>> failed to log outgoing url button body:", str(e)) |
|
|
| except Exception as e: |
| print(">>> failed to send url button:", str(e)) |
| fallback_reply = action.get("body", reply) or "تمام، افتح الرابط من فضلك." |
| try: |
| status, txt = send_text_message(device_key, chat_id, fallback_reply) |
| print(">>> url fallback send status:", status) |
| print(">>> url fallback send body:", txt) |
| log_message(customer_phone, bot_number, "out", fallback_reply) |
| except Exception as send_err: |
| print(">>> failed to send url fallback:", str(send_err)) |
|
|
| else: |
| try: |
| status, txt = send_text_message(device_key, chat_id, reply) |
| print(">>> sent text status:", status) |
| print(">>> sent text body:", txt) |
|
|
| try: |
| log_message(customer_phone, bot_number, "out", reply) |
| except Exception as e: |
| print(">>> failed to log outgoing text:", str(e)) |
|
|
| except Exception as e: |
| print(">>> failed to send text:", str(e)) |
|
|
| if action and action.get("type") == "handoff": |
| department = action.get("department") |
| summary = action.get("summary", "") |
| metadata = result.get("flow_data", {}) |
|
|
| try: |
| if department == "sales": |
| create_sales_handoff( |
| customer_phone=customer_phone, |
| bot_number=bot_number, |
| summary=summary, |
| metadata=metadata |
| ) |
| elif department == "support": |
| create_support_handoff( |
| customer_phone=customer_phone, |
| bot_number=bot_number, |
| summary=summary, |
| metadata=metadata |
| ) |
| print(">>> handoff created:", department) |
| except Exception as e: |
| print(">>> failed to create handoff:", str(e)) |
|
|
| return { |
| "ok": True, |
| "device_key": device_key, |
| "customer_phone": customer_phone, |
| "state": result.get("next_state"), |
| "has_action": bool(action), |
| } |
|
|
|
|
| @app.post("/complaints/callback") |
| async def complaints_callback(req: Request): |
| print("========== /complaints/callback HIT ==========") |
|
|
| try: |
| payload = await req.json() |
| except Exception: |
| return JSONResponse(status_code=400, content={"ok": False, "error": "invalid json"}) |
|
|
| print("===== COMPLAINT CALLBACK =====") |
| print(json.dumps(payload, ensure_ascii=False, indent=2)) |
| print("==============================") |
|
|
| uid = payload.get("uid", "") |
| complaint_number = payload.get("complaint_number", "") |
| status = payload.get("status", "") |
|
|
| return { |
| "ok": True, |
| "uid": uid, |
| "complaint_number": complaint_number, |
| "status": status |
| } |