Spaces:
Sleeping
Sleeping
| import json | |
| import re | |
| import requests | |
| from fastapi import FastAPI, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse | |
| # ========================= | |
| # Config | |
| # ========================= | |
| GREENAPI_INSTANCE_ID = "7105210836" | |
| GREENAPI_TOKEN = "805b69f6c85d4e6caea0edaba692b889abecc9e6bb8b457e8f" | |
| GREENAPI_BASE = f"https://7105.api.greenapi.com/waInstance{GREENAPI_INSTANCE_ID}" | |
| # Interactive buttons endpoint | |
| GREENAPI_SEND_URL_BUTTON_URL = f"{GREENAPI_BASE}/sendInteractiveButtons/{GREENAPI_TOKEN}" | |
| # Interactive REPLY buttons endpoint | |
| GREENAPI_SEND_REPLY_BUTTONS_URL = f"{GREENAPI_BASE}/sendInteractiveButtonsReply/{GREENAPI_TOKEN}" | |
| # Plain text endpoint (لو عندك endpoint مختلف في GreenAPI غيّره هنا) | |
| GREENAPI_SEND_TEXT_URL = ( | |
| f"https://7105.api.greenapi.com/" | |
| f"waInstance{GREENAPI_INSTANCE_ID}/sendMessage/{GREENAPI_TOKEN}" | |
| ) | |
| COMPLAINT_FORM_URL = "https://mrhelp92.github.io/Complaint-form/" # <-- حط لينك فورم الشكوى الحقيقي | |
| AI_BOT_URL = "https://mr-help-adkbase.hf.space/processtext" # <-- endpoint بتاع البوت | |
| TIMEOUT_SEC = 60 | |
| # (لو عايز تفضل GAS زي ما هو موجود) | |
| GAS_WEBAPP_URL = "https://script.google.com/macros/s/AKfycbxFLlPzSvXM6xpVXR3-BjoK1lkNjYb8o1pctXI4Q2NZvv6wqKnIaSQA6hhL5lP_TQbTgQ/exec" | |
| app = FastAPI() | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=False, | |
| allow_methods=["POST", "OPTIONS"], | |
| allow_headers=["*"], | |
| ) | |
| # ========================= | |
| # Intent detection (بسيط) | |
| # ========================= | |
| GREET_WORDS = [ | |
| "اهلا", "أهلا", "مرحبا", "هاي", "hello", "hi", | |
| "السلام عليكم", "سلام عليكم", "مساء الخير", "صباح الخير", | |
| "مساء الفل", "صباح الفل" | |
| ] | |
| COMPLAINT_WORDS = [ | |
| "شكوى", "شكاوي", "اشتكي", "مشكلة", "مشكلتي", "تذمر", "زعلان", | |
| "سيء", "خدمة سيئة", "عايز أشتكي", "عايز اشتكي", "complaint" | |
| ] | |
| def detect_intent_simple(text: str) -> str: | |
| t = (text or "").strip().lower() | |
| if any(w in t for w in [x.lower() for x in GREET_WORDS]): | |
| return "GREETING" | |
| if any(w in t for w in [x.lower() for x in COMPLAINT_WORDS]): | |
| return "COMPLAINT" | |
| return "OTHER" | |
| # ========================= | |
| # GreenAPI send helpers | |
| # ========================= | |
| def send_url_button(chat_id: str, header: str, body: str, footer: str, url: str, button_text: str = "تسجيل الشكوى"): | |
| payload = { | |
| "chatId": chat_id, | |
| "body": body, | |
| "header": header, | |
| "footer": footer, | |
| "buttons": [ | |
| { | |
| "type": "url", | |
| "buttonId": "1", | |
| "buttonText": button_text, | |
| "url": url | |
| } | |
| ] | |
| } | |
| resp = requests.post(GREENAPI_SEND_URL_BUTTON_URL, json=payload, timeout=TIMEOUT_SEC) | |
| return resp.status_code, (resp.text or "")[:1500] | |
| def send_reply_buttons(chat_id: str, header: str, body: str, footer: str, buttons: list): | |
| """ | |
| buttons example: | |
| [{"buttonId":"1","buttonText":"..."}, ...] | |
| """ | |
| payload = { | |
| "chatId": chat_id, | |
| "body": body, | |
| "header": header, | |
| "footer": footer, | |
| "buttons": buttons | |
| } | |
| resp = requests.post(GREENAPI_SEND_REPLY_BUTTONS_URL, json=payload, timeout=TIMEOUT_SEC) | |
| return resp.status_code, (resp.text or "")[:1500] | |
| def send_text_message(chat_id: str, message: str): | |
| payload = { | |
| "chatId": chat_id, | |
| "message": message | |
| } | |
| headers = { | |
| "Content-Type": "application/json" | |
| } | |
| resp = requests.post( | |
| GREENAPI_SEND_TEXT_URL, | |
| json=payload, | |
| headers=headers, | |
| timeout=TIMEOUT_SEC | |
| ) | |
| return resp.status_code, (resp.text or "")[:1500] | |
| # ========================= | |
| # AI bot call | |
| # ========================= | |
| def call_ai_bot(message: str): | |
| payload = {"message": message} | |
| resp = requests.post(AI_BOT_URL, json=payload, timeout=TIMEOUT_SEC) | |
| resp.raise_for_status() | |
| try: | |
| return resp.json() | |
| except Exception: | |
| return {"ok": False, "raw": resp.text} | |
| # ========================= | |
| # Webhook receiver | |
| # ========================= | |
| def extract_text_message(msg_data: dict) -> str: | |
| """ | |
| يرجّع نص المستخدم سواء كان: | |
| - textMessage | |
| - extendedTextMessage | |
| - interactiveButtonsResponse (ضغط زرار) | |
| """ | |
| if not isinstance(msg_data, dict): | |
| return "" | |
| t = (msg_data.get("typeMessage") or "").strip() | |
| # 1) رسالة نصية عادية | |
| if t == "textMessage": | |
| return ((msg_data.get("textMessageData") or {}).get("textMessage") or "").strip() | |
| # 2) رسالة نصية ممتدة (شائع) | |
| if t == "extendedTextMessage": | |
| return ((msg_data.get("extendedTextMessageData") or {}).get("text") or "").strip() | |
| # 3) ضغط زرار (Interactive Buttons Response) | |
| if t == "interactiveButtonsResponse": | |
| r = msg_data.get("interactiveButtonsResponse") or {} | |
| # أهم حاجة: selectedDisplayText (نص الزرار) | |
| txt = (r.get("selectedDisplayText") or "").strip() | |
| if txt: | |
| return txt | |
| # fallback: selectedId | |
| return (r.get("selectedId") or "").strip() | |
| # fallback عام | |
| for path in [ | |
| ("textMessageData", "textMessage"), | |
| ("extendedTextMessageData", "text"), | |
| ("interactiveButtonsResponse", "selectedDisplayText"), | |
| ("interactiveButtonsResponse", "selectedId"), | |
| ("text",), | |
| ("message",), | |
| ]: | |
| cur = msg_data | |
| for k in path: | |
| cur = cur.get(k) if isinstance(cur, dict) else None | |
| if isinstance(cur, str) and cur.strip(): | |
| return cur.strip() | |
| return "" | |
| async def webhook_receiver(req: Request): | |
| try: | |
| body = await req.json() | |
| except Exception: | |
| raw = await req.body() | |
| print("===== RECEIVED RAW =====") | |
| print(raw) | |
| print("========================") | |
| return {"ok": True} | |
| print("===== RECEIVED JSON =====") | |
| print(json.dumps(body, ensure_ascii=False, indent=2)) | |
| print("=========================") | |
| # 1) فلتر نوع الويبهوك | |
| if body.get("typeWebhook") != "incomingMessageReceived": | |
| return {"ok": True, "ignored": True} | |
| # 2) استخرج wid + chatId + الرسالة | |
| instance_wid = (body.get("instanceData") or {}).get("wid") # ده wid بتاع الـ instance | |
| sender_data = body.get("senderData") or {} | |
| chat_id = sender_data.get("chatId") # ده اللي لازم تبعت له الرد | |
| msg_data = body.get("messageData") or {} | |
| type_message = (msg_data.get("typeMessage") or "") | |
| text_message = extract_text_message(msg_data) | |
| print(">>> instance_wid:", instance_wid) | |
| print(">>> chat_id:", chat_id) | |
| print(">>> typeMessage:", type_message) | |
| print(">>> text_message:", text_message) | |
| # لو مفيش chat_id أو مفيش رسالة نصية، نوقف | |
| if not chat_id or not text_message: | |
| return {"ok": True, "skipped": True} | |
| # 3) حدّد intent | |
| intent = detect_intent_simple(text_message) | |
| print(">>> intent:", intent) | |
| # 4) ردود حسب intent | |
| if intent == "GREETING": | |
| header = "أهلاً بيك في ÄDK 👋" | |
| body_txt = ( | |
| "منورنا! 😊\n" | |
| "أنا مساعد ÄDK، تحب اساعدك ازاي؟" | |
| ) | |
| footer = "ÄDK - German Courses" | |
| buttons = [ | |
| {"buttonId": "1", "buttonText": "أنواع الكورسات"}, | |
| {"buttonId": "2", "buttonText": "تحديد مستوى"}, | |
| {"buttonId": "3", "buttonText": "تواصل معنا"}, | |
| ] | |
| status, txt = send_reply_buttons(chat_id, header, body_txt, footer, buttons) | |
| print(">>> sent GREETING buttons:", status, txt) | |
| return {"ok": True, "intent": intent} | |
| if intent == "COMPLAINT": | |
| header = "احنا هنا عشان نساعدك 🤝" | |
| body_txt = "يسعدنا تواصلك. سجل الشكوى من الرابط ده، وفريقنا هيتواصل معاك في أقرب وقت." | |
| footer = "شكراً لاختيار ÄDK" | |
| status, txt = send_url_button( | |
| chat_id=chat_id, | |
| header=header, | |
| body=body_txt, | |
| footer=footer, | |
| url=COMPLAINT_FORM_URL, | |
| button_text="تسجيل الشكوى" | |
| ) | |
| print(">>> sent COMPLAINT url button:", status, txt) | |
| return {"ok": True, "intent": intent} | |
| # 5) غير كده → نبعته للـ AI bot | |
| try: | |
| bot_res = call_ai_bot(text_message) | |
| # نتوقع {ok, intent, reply} | |
| reply = "" | |
| if isinstance(bot_res, dict): | |
| reply = (bot_res.get("reply") or "").strip() | |
| if not reply: | |
| reply = "تمام — وصلت رسالتك. ممكن توضّحلي قصدك أكتر؟" | |
| status, txt = send_text_message(chat_id, reply) | |
| print(">>> sent AI reply:", status, txt) | |
| return {"ok": True, "intent": "AI", "bot_intent": (bot_res.get("intent") if isinstance(bot_res, dict) else None)} | |
| except Exception as e: | |
| print("❌ AI bot failed:", str(e)) | |
| # fallback لطيف | |
| status, txt = send_text_message(chat_id, "تمام — حصلت مشكلة بسيطة. ممكن تعيد إرسال رسالتك تاني؟") | |
| print(">>> sent fallback:", status, txt) | |
| return JSONResponse(status_code=200, content={"ok": True, "intent": "AI_FAILED", "error": str(e)}) | |
| # ========================= | |
| # Complaints proxy (زي ما هو عندك) | |
| # ========================= | |
| async def complaints_proxy(req: Request): | |
| try: | |
| payload = await req.json() | |
| except Exception as e: | |
| print("❌ Failed to parse incoming JSON:", str(e)) | |
| return JSONResponse(status_code=400, content={"ok": False, "error": "invalid json body"}) | |
| print("========== INCOMING PAYLOAD ==========") | |
| print(json.dumps(payload, ensure_ascii=False, indent=2)) | |
| print("======================================") | |
| try: | |
| gas_resp = requests.post( | |
| GAS_WEBAPP_URL, | |
| json=payload, | |
| headers={"Content-Type": "application/json"}, | |
| timeout=60 | |
| ) | |
| except Exception as e: | |
| print("❌ ERROR calling GAS:", repr(e)) | |
| return JSONResponse( | |
| status_code=502, | |
| content={"ok": False, "error": "failed to reach GAS", "details": str(e)} | |
| ) | |
| print("========== GAS STATUS ==========") | |
| print(gas_resp.status_code) | |
| print("========== GAS RAW TEXT (first 1500 chars) ==========") | |
| print((gas_resp.text or "")[:1500]) | |
| print("=====================================================") | |
| try: | |
| gas_json = gas_resp.json() | |
| return JSONResponse(status_code=200, content=gas_json) | |
| except Exception: | |
| return JSONResponse( | |
| status_code=200, | |
| content={ | |
| "ok": False, | |
| "error": "GAS returned non-JSON", | |
| "gas_status": gas_resp.status_code, | |
| "raw": (gas_resp.text or "")[:1500] | |
| } | |
| ) |