From d8eab55aac8f5b1b1f7d6301c18c674a56d29d6b Mon Sep 17 00:00:00 2001 From: Floke Date: Tue, 10 Mar 2026 07:38:47 +0000 Subject: [PATCH] feat(smartlead): Add Smartlead webhook integration [31f88f42] --- lead-engine/README.md | 20 ++++++++++++ lead-engine/trading_twins/manager.py | 46 +++++++++++++++++++++++++++- nginx-proxy-clean.conf | 14 +++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/lead-engine/README.md b/lead-engine/README.md index 31bc6314..6ea969fc 100644 --- a/lead-engine/README.md +++ b/lead-engine/README.md @@ -34,6 +34,26 @@ Der vollautomatische "Zero Touch" Workflow für Trading Twins Anfragen. * **Abstand:** Bietet zwei Termine an, mit ca. **3 Stunden Pause** dazwischen. * **Buchung:** Klick auf Link -> Server erstellt Outlook-Termin von `info@` mit `e.melcer` als Teilnehmer. +### 6. Smartlead Webhook Integration (NEU) +* **Zweck:** Empfang von Echtzeit-Lead-Benachrichtigungen direkt aus der Smartlead-Plattform. +* **Event-basiert:** Das System empfängt nur Daten für *neue* Ereignisse (z.B. "Lead als heiß markiert"), keine historischen Daten. + +#### Endpunkte +Die folgenden Endpunkte werden für Smartlead bereitgestellt. Die Basis-URL ist flexibel und kann bei einem Serverumzug einfach angepasst werden. Der Platzhalter `{IHRE_AKTUELLE_DOMAIN}` sollte durch die aktuell aktive URL (z.B. `floke-ai.duckdns.org` oder die zukünftige neue Domain) ersetzt werden. + +* **Hot Leads:** `https://{IHRE_AKTUELLE_DOMAIN}/public/smartlead/hot-lead` +* **Follow-up Leads:** `https://{IHRE_AKTUELLE_DOMAIN}/public/smartlead/follow-up-lead` + +#### Inbetriebnahme & Test-Prozess +Die Integration wird in einem kontrollierten Prozess ausgerollt, um die Datenstruktur sicher zu analysieren: + +1. **URLs an Smartlead übergeben:** Die oben genannten URLs werden an den Smartlead-Support oder in deren Admin-Oberfläche eingetragen. +2. **Test-Auslösung anfordern:** Smartlead wird gebeten, für jeden der beiden Endpunkte einen einzelnen Test-Lead manuell auszulösen. +3. **Datenanalyse:** Alle eingehenden Anfragen werden ungefiltert in die Log-Datei `lead-engine/Log/smartlead_webhooks.log` geschrieben. Dies ermöglicht eine genaue Analyse der von Smartlead gesendeten JSON-Struktur. +4. **Implementierung der Logik:** Basierend auf den analysierten Test-Daten wird die eigentliche Geschäftslogik (z.B. Eintrag in die Datenbank, Teams-Benachrichtigung) implementiert. + +Dieser Prozess stellt sicher, dass wir nicht "blind" entwickeln, sondern auf Basis der realen Datenstruktur von Smartlead aufbauen. + ## 🏗 Architektur ```text diff --git a/lead-engine/trading_twins/manager.py b/lead-engine/trading_twins/manager.py index 559f6156..0cafab7c 100644 --- a/lead-engine/trading_twins/manager.py +++ b/lead-engine/trading_twins/manager.py @@ -10,7 +10,7 @@ from zoneinfo import ZoneInfo from threading import Thread, Lock import uvicorn import logging -from fastapi import FastAPI, Response, BackgroundTasks +from fastapi import FastAPI, Request, Response, BackgroundTasks from fastapi.responses import HTMLResponse from pydantic import BaseModel from sqlalchemy.orm import sessionmaker @@ -206,6 +206,50 @@ def create_calendar_invite(lead_email, company, start_time): # --- FastAPI Server --- app = FastAPI() +# --- Webhook Endpoints for Smartlead --- +SMARTLEAD_LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "Log") +SMARTLEAD_LOG_FILE = os.path.join(SMARTLEAD_LOG_DIR, "smartlead_webhooks.log") + +# Ensure log directory exists +os.makedirs(SMARTLEAD_LOG_DIR, exist_ok=True) + +async def log_webhook_data(webhook_type: str, request: Request): + """Helper function to log incoming webhook data.""" + try: + data = await request.json() + timestamp = datetime.now(TZ_BERLIN).isoformat() + + log_entry = { + "timestamp": timestamp, + "webhook_type": webhook_type, + "source_ip": request.client.host, + "payload": data + } + + with open(SMARTLEAD_LOG_FILE, "a", encoding="utf-8") as f: + f.write(json.dumps(log_entry) + "\n") + + logging.info(f"Successfully processed and logged '{webhook_type}' webhook.") + return {"status": "success", "message": f"{webhook_type} webhook received"} + + except json.JSONDecodeError: + logging.error("Webhook received with invalid JSON format.") + return Response(content='{"status": "error", "message": "Invalid JSON format"}', status_code=400, media_type="application/json") + except Exception as e: + logging.error(f"Error processing '{webhook_type}' webhook: {e}") + return Response(content='{"status": "error", "message": "Internal server error"}', status_code=500, media_type="application/json") + +@app.post("/webhook/hot-lead", status_code=202) +async def webhook_hot_lead(request: Request): + """Webhook endpoint for 'hot leads' from Smartlead.""" + return await log_webhook_data("hot-lead", request) + +@app.post("/webhook/follow-up-lead", status_code=202) +async def webhook_follow_up_lead(request: Request): + """Webhook endpoint for 'follow-up leads' from Smartlead.""" + return await log_webhook_data("follow-up-lead", request) +# --- END Webhook Endpoints --- + @app.get("/test_lead", status_code=202) def trigger_test_lead(background_tasks: BackgroundTasks): req_id = f"test_{int(time.time())}" diff --git a/nginx-proxy-clean.conf b/nginx-proxy-clean.conf index 36796f76..9f06f34f 100644 --- a/nginx-proxy-clean.conf +++ b/nginx-proxy-clean.conf @@ -131,6 +131,20 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + # Smartlead Webhooks (public) + location /public/smartlead/ { + auth_basic off; + # Rewrite the URL to remove the public prefix and pass the rest to the webhook handler + # e.g., /public/smartlead/hot-lead -> /webhook/hot-lead + rewrite ^/public/smartlead/(.*)$ /webhook/$1 break; + + proxy_pass http://lead-engine:8004; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /connector/ { auth_basic off; proxy_pass http://connector-superoffice:8000/;