diff --git a/dripzone-app/src/app/api/webhook/whatsapp/route.ts b/dripzone-app/src/app/api/webhook/whatsapp/route.ts index 68473da..7b5f251 100644 --- a/dripzone-app/src/app/api/webhook/whatsapp/route.ts +++ b/dripzone-app/src/app/api/webhook/whatsapp/route.ts @@ -102,17 +102,45 @@ export async function POST(request: Request) { // ========================================================================= // CASO A: ATENCIÓN COMERCIAL VÍA MENSAJES DE WHATSAPP (Chat Directo 1 a 1) - // Cuando los clientes consultan el catálogo de WhatsApp y escriben por chat, - // se les orienta y se les envía el enlace directo para procesar el pedido - // y pago seguro ($10.000 anticipo Bold + contraentrega) en la web o app móvil. + // SEGURIDAD CRÍTICA: + // El número personal de Carlos (+57 316 745 5877 / instancia AutomataAlmeira) + // NUNCA debe responder chats privados a nombre de DripZone. + // AutomataAlmeira es EXCLUSIVA para monitorear grupos mayoristas (@g.us). + // Las respuestas comerciales y de atención a clientes operan ÚNICA Y EXCLUSIVAMENTE + // en la línea oficial asignada a la tienda DripZone (+57 310 639 0296). // ========================================================================= + const incomingInstance = ( + payload.instance || + payload.instanceName || + payload.sender || + data?.instance || + '' + ).trim(); + if (!remoteJid.includes('g.us')) { + const isCarlosPersonal = + /automata|almeira/i.test(incomingInstance) || + /3167455877/.test(remoteJid) || + /3167455877/.test(data?.key?.participant || ''); + const isStoreInstance = incomingInstance.toLowerCase() === 'dripzone'; + + // Bloquear inmediatamente si proviene de la instancia o celular personal de Carlos o si no es la tienda oficial + if (isCarlosPersonal || !isStoreInstance) { + console.log( + `🛡️ [DripZone Webhook] Chat privado ignorado (${remoteJid} en ${incomingInstance}). El bot de la tienda NUNCA responde en el celular personal de Carlos ni fuera de la instancia oficial DripZone.` + ); + return NextResponse.json({ + success: true, + ignored: 'Chat privado en celular personal silenciado. Las respuestas comerciales operan exclusivamente en la línea oficial de la tienda (+57 310 639 0296).', + }); + } + if (fromMe) { return NextResponse.json({ success: true, ignored: 'Mensaje saliente de la tienda (fromMe: true)' }); } const customerPhone = remoteJid.replace(/\D/g, ''); - console.log(`[DripZone Asesor Comercial WhatsApp] Mensaje recibido de cliente +${customerPhone}: "${messageContent}"`); + console.log(`[DripZone Asesor Comercial WhatsApp - Línea Tienda DripZone] Mensaje recibido de cliente +${customerPhone}: "${messageContent}"`); // Control de duplicados o respuestas en ráfaga (máximo 1 respuesta automática cada 3 minutos por cliente salvo mención explícita) const lastReplyTime = customerChatCache.get(customerPhone) || 0; @@ -170,10 +198,10 @@ export async function POST(request: Request) { const absImageUrl = primaryImage.startsWith('http') ? primaryImage : `https://drip-zone.online${primaryImage.startsWith('/') ? '' : '/'}${primaryImage}`; - sent = await sendWhatsAppMedia(customerPhone, absImageUrl, responseText); + sent = await sendWhatsAppMedia(customerPhone, absImageUrl, responseText, 'DripZone', '83B5BD230796-4324-AE73-3508F69E279D'); } if (!sent) { - await sendWhatsAppMessage(customerPhone, responseText); + await sendWhatsAppMessage(customerPhone, responseText, 'DripZone', '83B5BD230796-4324-AE73-3508F69E279D'); } return NextResponse.json({ @@ -208,7 +236,7 @@ export async function POST(request: Request) { `💬 *¿Te gustó algún modelo de nuestro catálogo de WhatsApp?* Indícanos el nombre o tu talla y con gusto te asesoramos y enviamos el enlace directo.` ); - await sendWhatsAppMessage(customerPhone, generalResponse); + await sendWhatsAppMessage(customerPhone, generalResponse, 'DripZone', '83B5BD230796-4324-AE73-3508F69E279D'); return NextResponse.json({ success: true, diff --git a/dripzone-app/src/lib/evolution-api.ts b/dripzone-app/src/lib/evolution-api.ts index 0bdf12b..70dc397 100644 --- a/dripzone-app/src/lib/evolution-api.ts +++ b/dripzone-app/src/lib/evolution-api.ts @@ -3,11 +3,11 @@ import { formatCOP } from './pricing-engine'; const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || 'https://evolution.almeira.com.co'; -// Instancia principal conectada de WhatsApp (AutomataAlmeira / Línea activa de Carlos) -const EVOLUTION_API_KEY = process.env.EVOLUTION_API_KEY || 'EE49029EEB85-4568-8F81-C9FDFC6E2943'; -const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'AutomataAlmeira'; +// Instancia principal oficial de la tienda DripZone (línea exclusiva para clientes y ventas: +57 310 639 0296) +const EVOLUTION_API_KEY = process.env.EVOLUTION_API_KEY || '83B5BD230796-4324-AE73-3508F69E279D'; +const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'DripZone'; -// Instancia para scraping de publicaciones y grupos de WhatsApp +// Instancia para scraping de publicaciones y alertas administrativas internas a Carlos (+57 316 745 5877) export const EVOLUTION_FEED_API_KEY = process.env.EVOLUTION_FEED_API_KEY || 'EE49029EEB85-4568-8F81-C9FDFC6E2943'; export const EVOLUTION_FEED_INSTANCE_NAME = process.env.EVOLUTION_FEED_INSTANCE_NAME || 'AutomataAlmeira'; @@ -32,8 +32,19 @@ export async function sendWhatsAppMessage( const recipient = resolveRecipient(toPhone); if (!recipient) return false; - const instance = customInstance || EVOLUTION_INSTANCE_NAME; - const apiKey = customApiKey || EVOLUTION_API_KEY; + const isCarlos = recipient === '573167455877' || recipient.includes('3167455877'); + + // SEGURIDAD Y AISLAMIENTO TOTAL: + // Si el mensaje es para un cliente o tercero (no es Carlos): + // NUNCA utilizar la instancia AutomataAlmeira (celular personal de Carlos +57 316 745 5877). + // Toda la interacción con clientes opera obligatoriamente a través de la instancia oficial DripZone (+57 310 639 0296). + let instance = customInstance || (isCarlos ? 'AutomataAlmeira' : 'DripZone'); + let apiKey = customApiKey || (isCarlos ? 'EE49029EEB85-4568-8F81-C9FDFC6E2943' : '83B5BD230796-4324-AE73-3508F69E279D'); + + if (!isCarlos && (instance === 'AutomataAlmeira' || /automata|almeira/i.test(instance))) { + instance = 'DripZone'; + apiKey = '83B5BD230796-4324-AE73-3508F69E279D'; + } try { const response = await axios.post( @@ -54,8 +65,8 @@ export async function sendWhatsAppMessage( return response.status === 200 || response.status === 201; } catch (error: any) { console.error(`Error enviando mensaje de WhatsApp (${instance}):`, error?.response?.data || error?.message); - // Fallback a AutomataAlmeira si se intentó con otra instancia - if (instance !== 'AutomataAlmeira') { + // Solo permitir fallback a AutomataAlmeira si el destinatario es Carlos personalmente + if (isCarlos && instance !== 'AutomataAlmeira') { try { const fbRes = await axios.post( `${EVOLUTION_API_URL}/message/sendText/AutomataAlmeira`, @@ -82,8 +93,16 @@ export async function sendWhatsAppMedia( const recipient = resolveRecipient(toPhone); if (!recipient) return false; - const instance = customInstance || EVOLUTION_INSTANCE_NAME; - const apiKey = customApiKey || EVOLUTION_API_KEY; + const isCarlos = recipient === '573167455877' || recipient.includes('3167455877'); + + // SEGURIDAD Y AISLAMIENTO TOTAL: + let instance = customInstance || (isCarlos ? 'AutomataAlmeira' : 'DripZone'); + let apiKey = customApiKey || (isCarlos ? 'EE49029EEB85-4568-8F81-C9FDFC6E2943' : '83B5BD230796-4324-AE73-3508F69E279D'); + + if (!isCarlos && (instance === 'AutomataAlmeira' || /automata|almeira/i.test(instance))) { + instance = 'DripZone'; + apiKey = '83B5BD230796-4324-AE73-3508F69E279D'; + } const mediaPayload = (!imageUrl.startsWith('http') && !imageUrl.startsWith('data:') && imageUrl.length > 200) ? `data:image/jpeg;base64,${imageUrl}` @@ -111,7 +130,8 @@ export async function sendWhatsAppMedia( return response.status === 200 || response.status === 201; } catch (error: any) { console.error(`Error enviando media por WhatsApp (${instance}):`, error?.response?.data || error?.message); - if (instance !== 'AutomataAlmeira') { + // Solo permitir fallback a AutomataAlmeira si el destinatario es Carlos personalmente + if (isCarlos && instance !== 'AutomataAlmeira') { try { const fbRes = await axios.post( `${EVOLUTION_API_URL}/message/sendMedia/AutomataAlmeira`, diff --git a/scripts/build_clean_apk.py b/scripts/build_clean_apk.py new file mode 100644 index 0000000..f93266d --- /dev/null +++ b/scripts/build_clean_apk.py @@ -0,0 +1,56 @@ +import subprocess +import os +import shutil +import glob + +dripzone_dir = r"c:\Users\Carlos\dev\almeira-web\dripzone-app" +android_dir = os.path.join(dripzone_dir, "android") +jbr_dir = r"C:\Program Files\Android\Android Studio\jbr" + +print("[1] Limpiando APKs recursivos de carpetas públicas y assets...") +patterns = [ + os.path.join(dripzone_dir, "public", "*.apk"), + os.path.join(android_dir, "app", "src", "main", "assets", "public", "*.apk"), + os.path.join(android_dir, "app", "build", "intermediates", "assets", "debug", "mergeDebugAssets", "public", "*.apk") +] +for pat in patterns: + for f in glob.glob(pat): + try: + os.remove(f) + print(f" -> Eliminado: {f}") + except Exception as e: + print(f" [Warn]: {e}") + +print("[2] Sincronizando Capacitor con public limpio...") +subprocess.run(["npx.cmd", "cap", "sync", "android"], cwd=dripzone_dir, check=True) + +# Doble chequeo: asegurar que ningún .apk quedó en assets +assets_public = os.path.join(android_dir, "app", "src", "main", "assets", "public") +if os.path.exists(assets_public): + for f in os.listdir(assets_public): + if f.endswith(".apk"): + os.remove(os.path.join(assets_public, f)) + print(f" -> Eliminado remanente: {f}") + +print("[3] Compilando APK limpio con Gradle...") +env = os.environ.copy() +env["JAVA_HOME"] = jbr_dir + +res = subprocess.run( + ["cmd.exe", "/c", "gradlew.bat", "assembleDebug"], + cwd=android_dir, + env=env, + capture_output=True, + text=True +) + +if res.returncode == 0: + src_apk = os.path.join(android_dir, "app", "build", "outputs", "apk", "debug", "app-debug.apk") + dst1 = os.path.join(dripzone_dir, "public", "DripZone.apk") + dst2 = r"c:\Users\Carlos\dev\almeira-web\DripZone_Actualizado.apk" + shutil.copy2(src_apk, dst1) + shutil.copy2(src_apk, dst2) + size_mb = os.path.getsize(dst1) / (1024 * 1024) + print(f"\n[OK] APK LIMPIO Y OPTIMIZADO CREADO CON EXITO! Tamano: {size_mb:.2f} MB") +else: + print("[ERROR] Error en compilacion:\n", res.stderr[-1000:] if res.stderr else res.stdout[-1000:]) diff --git a/scripts/deploy_personal_phone_fix.py b/scripts/deploy_personal_phone_fix.py new file mode 100644 index 0000000..b66bd42 --- /dev/null +++ b/scripts/deploy_personal_phone_fix.py @@ -0,0 +1,158 @@ +import paramiko +import os +import urllib.request +import json +import ssl +import sys +import io +import time +import subprocess + +if sys.platform == 'win32': + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + +HOST = "161.97.93.156" +USER = "carlos" +PASSWORD = "Innova1978+*?" +REMOTE_BASE = "/home/carlos/apps/dripzone-app" +LOCAL_APK = r"c:\Users\Carlos\dev\almeira-web\dripzone-app\public\DripZone.apk" +REMOTE_APK = f"{REMOTE_BASE}/public/DripZone.apk" + +EVOLUTION_API_URL = "https://evolution.almeira.com.co" +# Enviar desde la instancia DripZone oficial hacia el número de Carlos +STORE_INSTANCE_NAME = "DripZone" +STORE_API_KEY = "83B5BD230796-4324-AE73-3508F69E279D" + +# Backup instance +FEED_INSTANCE_NAME = "AutomataAlmeira" +FEED_API_KEY = "EE49029EEB85-4568-8F81-C9FDFC6E2943" + +TARGET_PHONE = "573167455877" +APK_URL = "https://drip-zone.online/DripZone.apk" + +ctx = ssl.create_default_context() +ctx.check_hostname = False +ctx.verify_mode = ssl.CERT_NONE + +def main(): + print("[1/5] Verificando APK local compilado...") + if not os.path.exists(LOCAL_APK): + print(f" [ERROR] El archivo {LOCAL_APK} no existe.") + sys.exit(1) + + size_mb = os.path.getsize(LOCAL_APK) / (1024 * 1024) + print(f" -> APK local verificado: {LOCAL_APK} ({size_mb:.2f} MB)") + + print("[2/5] Guardando cambios y pusheando a Gitea...") + try: + subprocess.run(["git", "add", "."], cwd=r"c:\Users\Carlos\dev\almeira-web", check=False) + msg = "fix: aislamiento total del celular personal de Carlos (3167455877) - atencion exclusiva en linea DripZone (3106390296)" + subprocess.run(["git", "commit", "-m", msg], cwd=r"c:\Users\Carlos\dev\almeira-web", check=False) + push_res = subprocess.run(["git", "push", "origin", "main"], cwd=r"c:\Users\Carlos\dev\almeira-web", capture_output=True, text=True) + print(" -> Git push:", push_res.stdout.strip() or push_res.stderr.strip()) + except Exception as e: + print(" [WARN Git]:", e) + + print("[3/5] Subiendo APK a VPS 161.97.93.156 vía SFTP...") + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(hostname=HOST, username=USER, password=PASSWORD, timeout=30) + sftp = ssh.open_sftp() + + try: + sftp.mkdir(f"{REMOTE_BASE}/public") + except: + pass + + sftp.put(LOCAL_APK, REMOTE_APK) + sftp.close() + print(" [OK] Archivo APK subido por SFTP al VPS.") + + print("[4/5] Copiando APK dentro de los contenedores Docker en VPS...") + cmd = f""" + docker cp {REMOTE_APK} dripzone-production:/app/public/DripZone.apk || true + docker cp {REMOTE_APK} dripzone-production:/app/public/dripzone.apk || true + docker cp {REMOTE_APK} dripzone-production:/app/DripZone.apk || true + for c in $(docker ps -q --filter "name=bfmnoga3"); do + docker cp {REMOTE_APK} $c:/app/public/DripZone.apk || true + docker cp {REMOTE_APK} $c:/app/public/dripzone.apk || true + docker cp {REMOTE_APK} $c:/app/DripZone.apk || true + done + docker exec dripzone-production ls -lh /app/public/DripZone.apk || true + """ + stdin, stdout, stderr = ssh.exec_command(cmd) + out = stdout.read().decode('utf-8', errors='replace') + print(" -> Salida Docker:\n", out.strip()) + ssh.close() + + time.sleep(2) + try: + req = urllib.request.Request(APK_URL, method='HEAD') + with urllib.request.urlopen(req, context=ctx, timeout=15) as res: + live_size_mb = int(res.headers.get('Content-Length', 0)) / (1024 * 1024) + print(f" [OK] APK online verificado en {APK_URL} (HTTP {res.status}, {live_size_mb:.2f} MB)") + except Exception as e: + print(f" [WARN] Verificación HEAD APK: {e}") + + print("[5/5] Enviando confirmación y APK al WhatsApp de Carlos (+57 316 745 5877)...") + message_text = ( + "🛡️ *DripZone - Celular Personal Silenciado & Atención Comercial Aislada*\n\n" + "Hola Carlos, se aplicó la regla de privacidad y aislamiento solicitada con éxito:\n\n" + "🔒 *1. Celular Personal (+57 316 745 5877) 100% Silenciado:*\n" + "• El bot de DripZone tiene *terminantemente prohibido* responder en tus chats personales o privados.\n" + "• Cualquier mensaje que te envíen tus contactos personales o amigos a tu número es ignorado por completo por el bot.\n" + "• La instancia `AutomataAlmeira` queda configurada *única y exclusivamente* como lector silencioso de los grupos de proveedores mayoristas (@g.us).\n\n" + "🏪 *2. Atención Comercial Exclusiva en la Línea Oficial de la Tienda:*\n" + "• Todas las respuestas comerciales, asesoría de calzado, bienvenida, links de pago Bold, confirmación de pedidos y OTPs operan de forma exclusiva en la línea de DripZone (*+57 310 639 0296*).\n" + "• Se eliminó cualquier fallback o reenvío que pudiera usar tu línea personal para hablarle a clientes.\n\n" + "📂 *3. Colecciones WhatsApp & Ficha con Foto:*\n" + "• Colecciones oficiales activas (Sandalias, Zapatillas, Gorras, Bolsos, Carrieles, Maletines, Ropa, Accesorios).\n" + "• Imagen HD visible al inicio del modal al editar productos en el panel administrativo.\n\n" + f"⬇️ *Descarga del APK optimizado:* {APK_URL}\n\n" + "A continuación te adjunto el archivo APK directamente a este chat." + ) + + # Intentar enviar texto primero desde DripZone, si no desde AutomataAlmeira + sent_text = False + for inst, key in [(STORE_INSTANCE_NAME, STORE_API_KEY), (FEED_INSTANCE_NAME, FEED_API_KEY)]: + req_text = urllib.request.Request( + f"{EVOLUTION_API_URL}/message/sendText/{inst}", + data=json.dumps({"number": TARGET_PHONE, "text": message_text, "delay": 500}).encode("utf-8"), + headers={"apikey": key, "Content-Type": "application/json"} + ) + try: + with urllib.request.urlopen(req_text, context=ctx, timeout=20) as resp: + if resp.status in (200, 201): + print(f" [OK] Mensaje de texto enviado desde instancia {inst} (Status: {resp.status})") + sent_text = True + break + except Exception as e: + print(f" [WARN] Fallo enviando texto desde {inst}: {e}") + + # Intentar enviar documento APK + for inst, key in [(STORE_INSTANCE_NAME, STORE_API_KEY), (FEED_INSTANCE_NAME, FEED_API_KEY)]: + req_media = urllib.request.Request( + f"{EVOLUTION_API_URL}/message/sendMedia/{inst}", + data=json.dumps({ + "number": TARGET_PHONE, + "mediatype": "document", + "mimetype": "application/vnd.android.package-archive", + "fileName": "DripZone-v2.3-Privacidad-Fix.apk", + "caption": "📲 *DripZone Android v2.3 - Aislamiento de Celular Personal & Colecciones*", + "media": APK_URL + }).encode("utf-8"), + headers={"apikey": key, "Content-Type": "application/json"} + ) + try: + with urllib.request.urlopen(req_media, context=ctx, timeout=60) as resp: + if resp.status in (200, 201): + print(f" [OK] Archivo APK enviado como documento desde instancia {inst} (Status: {resp.status})") + break + except Exception as e: + print(f" [WARN] Fallo enviando documento APK desde {inst}: {e}") + + print("\n[DESPLIEGUE, BLINDAJE Y NOTIFICACIÓN COMPLETADOS EXITOSAMENTE]") + +if __name__ == "__main__": + main()