Mr-Help's picture
Update main.py
1d7d94d verified
import os, hmac, hashlib, uuid
from datetime import datetime, timezone
from urllib.parse import urlencode
import requests
from fastapi import FastAPI, Query, HTTPException
from pydantic import BaseModel, Field
app = FastAPI(title="Binrushd Lead Tracking API")
FORM_PUBLIC_URL = os.getenv(
"FORM_PUBLIC_URL",
"https://mrhelp92.github.io/Binrushd-CRM-Automation-Inquires_Form/"
)
GAS_WEBAPP_URL = os.getenv(
"GAS_WEBAPP_URL",
"https://script.google.com/macros/s/AKfycby8IZQ_YBQEiB4cwTcQORhAQmWtwOUAkb02M0Fu1wtDiiDVRwLpxV-rxsr4lSjAIKKt/exec"
)
SIGNING_SECRET = os.getenv("SIGNING_SECRET", "CHANGE_ME") # لازم قوي
# =========================
# Helpers
# =========================
def _b64url(data: bytes) -> str:
import base64
return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
def _hmac_sig(message: str) -> str:
sig = hmac.new(SIGNING_SECRET.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).digest()
return _b64url(sig)
def make_sig(lead_id: str, branch_id: str) -> str:
return _hmac_sig(f"{lead_id}|{branch_id}")
def verify_sig(lead_id: str, branch_id: str, sig: str) -> bool:
return hmac.compare_digest(make_sig(lead_id, branch_id), sig)
def gas_post(action: str, data: dict, timeout_sec: int = 25) -> dict:
"""
- دايمًا يرجّع dict فيه ok
- يمسك أي Timeout/Network Error بدل ما يوقع السيرفر
"""
payload = {"action": action, **data}
try:
r = requests.post(GAS_WEBAPP_URL, json=payload, timeout=timeout_sec)
except requests.exceptions.Timeout:
return {"ok": False, "error": f"GAS timeout after {timeout_sec}s", "action": action}
except requests.exceptions.RequestException as e:
return {"ok": False, "error": f"GAS request error: {str(e)}", "action": action}
try:
j = r.json()
except Exception:
return {"ok": False, "error": f"GAS invalid JSON: {r.text[:200]}", "action": action}
# لو GAS رجع ok:false
if not isinstance(j, dict):
return {"ok": False, "error": "GAS returned non-dict JSON", "action": action}
return j
# =========================
# Models
# =========================
class LinkResponse(BaseModel):
ok: bool = True
form_url: str
lead_id: str
branch_id: str
sig: str
class SubmitBody(BaseModel):
lead_id: str
branch_id: str
sig: str
contacted_at: str = Field(..., description="datetime-local from browser")
notes: str
# =========================
# GET /lead/link
# =========================
@app.get("/lead/link", response_model=LinkResponse)
def build_form_link(
lead_name: str = Query(..., min_length=1),
lead_phone: str = Query(..., min_length=6),
due_date: str = Query(..., min_length=4),
branch_email: str = Query(..., min_length=5),
):
if "@" not in branch_email:
raise HTTPException(status_code=400, detail="Invalid branch_email")
lead_id = uuid.uuid4().hex
branch_id = uuid.uuid4().hex
sig = make_sig(lead_id, branch_id)
gas_res = gas_post("createLead", {
"created_at": datetime.now(timezone.utc).isoformat(),
"lead_id": lead_id,
"branch_id": branch_id,
"sig": sig,
"lead_name": lead_name,
"lead_phone": lead_phone,
"due_date": due_date,
"branch_email": branch_email,
})
if not gas_res.get("ok"):
raise HTTPException(status_code=502, detail=f"GAS createLead failed: {gas_res.get('error','unknown')}")
qs = urlencode({"lead_id": lead_id, "branch_id": branch_id, "sig": sig})
form_url = f"{FORM_PUBLIC_URL}?{qs}"
return LinkResponse(form_url=form_url, lead_id=lead_id, branch_id=branch_id, sig=sig)
# =========================
# GET /form/open
# =========================
@app.get("/form/open")
def form_open(
lead_id: str = Query(...),
branch_id: str = Query(...),
sig: str = Query(...),
ua: str | None = Query(default=None),
):
if not verify_sig(lead_id, branch_id, sig):
return {"ok": False, "error": "الرابط غير صالح."}
lead = gas_post("getLead", {"lead_id": lead_id, "branch_id": branch_id})
if not lead.get("ok"):
# ✅ خليها واضحة بدل ما تبقى 200
raise HTTPException(status_code=502, detail=f"GAS getLead failed: {lead.get('error','unknown')}")
data = lead.get("data") or {}
stored_sig = data.get("sig", "")
if stored_sig and not hmac.compare_digest(stored_sig, sig):
return {"ok": False, "error": "الرابط غير صالح."}
# ✅ هنا الفرق: لازم نتحقق
open_res = gas_post("logOpen", {
"opened_at": datetime.now(timezone.utc).isoformat(),
"lead_id": lead_id,
"branch_id": branch_id,
"sig": sig,
"user_agent": ua or ""
})
# لو عايزها ما توقفش الصفحة: رجّع ok:true بس مع warning
if not open_res.get("ok"):
return {"ok": True, "data": data, "warning": f"logOpen failed: {open_res.get('error','unknown')}"}
return {"ok": True, "data": data}
# =========================
# POST /form/submit
# =========================
@app.post("/form/submit")
def form_submit(body: SubmitBody):
if not verify_sig(body.lead_id, body.branch_id, body.sig):
return {"ok": False, "error": "الرابط غير صالح."}
lead = gas_post("getLead", {"lead_id": body.lead_id, "branch_id": body.branch_id})
if not lead.get("ok"):
raise HTTPException(status_code=502, detail=f"GAS getLead failed: {lead.get('error','unknown')}")
data = lead.get("data") or {}
stored_sig = data.get("sig", "")
if stored_sig and not hmac.compare_digest(stored_sig, body.sig):
return {"ok": False, "error": "الرابط غير صالح."}
res = gas_post("submitFollowup", {
"submitted_at": datetime.now(timezone.utc).isoformat(),
"lead_id": body.lead_id,
"branch_id": body.branch_id,
"sig": body.sig,
"contacted_at": body.contacted_at,
"notes": body.notes
})
if not res.get("ok"):
raise HTTPException(status_code=502, detail=f"GAS submitFollowup failed: {res.get('error','unknown')}")
return {"ok": True}