import paramiko import tarfile import os import sys import io # Force UTF-8 stdout on Windows 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+*?" LOCAL_DIR = r"c:\Users\Carlos\dev\almeira-web\dripzone-app" ARCHIVE_NAME = r"c:\Users\Carlos\dev\almeira-web\dripzone_deploy.tar.gz" REMOTE_DIR = "/home/carlos/apps/dripzone-app" REMOTE_TAR = "/home/carlos/apps/dripzone_deploy.tar.gz" def create_tar(): print("[1/4] Empaquetando dripzone-app para despliegue de produccion...") with tarfile.open(ARCHIVE_NAME, "w:gz") as tar: for root, dirs, files in os.walk(LOCAL_DIR): # Excluir node_modules, .next, android (para mantener paquete ligero) dirs[:] = [d for d in dirs if d not in ['node_modules', '.next', 'android', '.git']] for file in files: if file.endswith('.apk'): continue full_path = os.path.join(root, file) rel_path = os.path.relpath(full_path, LOCAL_DIR) tar.add(full_path, arcname=rel_path) size_mb = os.path.getsize(ARCHIVE_NAME) / (1024 * 1024) print(f"[OK] Archivo generado: {ARCHIVE_NAME} ({size_mb:.2f} MB)") def deploy(): create_tar() print(f"[2/4] Conectando a VPS {HOST} via SSH / SFTP...") client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) connected = False for attempt in range(1, 4): try: print(f" Intento {attempt} de conexion a {HOST}...") client.connect(hostname=HOST, username=USER, password=PASSWORD, timeout=60, banner_timeout=60, auth_timeout=60) connected = True break except Exception as conn_err: print(f" Aviso: fallo en intento {attempt}: {conn_err}") import time time.sleep(2) if not connected: raise Exception(f"No se pudo conectar a {HOST} tras 3 intentos") sftp = client.open_sftp() print("[3/4] Subiendo archivo comprimido y APK al servidor...") sftp.put(ARCHIVE_NAME, REMOTE_TAR) apk_local = r"c:\Users\Carlos\dev\almeira-web\dripzone-app\public\DripZone.apk" if os.path.exists(apk_local): local_size = os.path.getsize(apk_local) remote_apk = f"{REMOTE_DIR}/public/DripZone.apk" should_upload = True try: r_stat = sftp.stat(remote_apk) if r_stat.st_size == local_size: print(f" -> DripZone.apk ({local_size/(1024*1024):.1f} MB) ya se encuentra actualizado en el VPS. Omitiendo subida redundante.") should_upload = False except: pass if should_upload: print(f" -> Subiendo DripZone.apk ({local_size/(1024*1024):.1f} MB) a public/...") try: sftp.mkdir(f"{REMOTE_DIR}/public") except: pass try: sftp.put(apk_local, remote_apk) print(" [OK] DripZone.apk subido al VPS.") except Exception as e: print(f" [Aviso] Error en subida SFTP de APK: {e}. Continuando con el despliegue...") sftp.close() print("[OK] Archivos base subidos exitosamente.") commands = f""" mkdir -p {REMOTE_DIR} tar -xzf {REMOTE_TAR} -C {REMOTE_DIR} rm -f {REMOTE_TAR} echo '{PASSWORD}' | sudo -S chown -R carlos:carlos /home/carlos/.docker || true chmod -R 777 /home/carlos/.docker || true cd {REMOTE_DIR} docker compose -f docker-compose.prod.yml down --remove-orphans || true DOCKER_BUILDKIT=1 docker compose -f docker-compose.prod.yml up -d --build sleep 3 docker exec -u 0 dripzone-production chown -R nextjs:nodejs /app/data /app/public || true docker exec -u 0 dripzone-production chmod -R 777 /app/data /app/public || true docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/public/DripZone.apk || true docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/DripZone.apk || true docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/public/dripzone.apk || true docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/public/app-dripzone.apk || true docker exec dripzone-production chmod 644 /app/public/DripZone.apk /app/DripZone.apk /app/public/dripzone.apk /app/public/app-dripzone.apk || true docker ps | grep dripzone """ print("[4/4] Ejecutando compilacion y levantando contenedor en Produccion...") stdin, stdout, stderr = client.exec_command(commands) while not stdout.channel.exit_status_ready(): line = stdout.readline() if line: print(line.strip()) for line in stdout.readlines(): print(line.strip()) err = stderr.read().decode('utf-8', errors='ignore') if err: print("[Logs/Avisos]:", err) status = stdout.channel.recv_exit_status() print(f"[EXIT STATUS]: {status}") client.close() if status == 0: send_apk_to_whatsapp() def send_apk_to_whatsapp(): import urllib.request import json print("[5/5] Enviando APK y reporte de actualizacion al WhatsApp de Carlos (+57 316 745 5877)...") url = "https://evolution.almeira.com.co/message/sendMedia/AutomataAlmeira" headers = { "apikey": "EE49029EEB85-4568-8F81-C9FDFC6E2943", "Content-Type": "application/json" } caption = ( "🚀 *¡PUBLICACIÓN AUTOMÁTICA EN VIVO & CATÁLOGO WHATSAPP SINCRONIZADO!* 👟📲\n" "━━━━━━━━━━━━━━━━━━━━━━\n" "✨ *Sistema Autónomo de Captura y Notificación 100% Activo:*\n\n" "1️⃣ 🌐 *Webhook Evolution API Reconfigurado y Conectado*: El webhook de la instancia `AutomataAlmeira` quedó apuntando directamente a `https://drip-zone.online/api/webhook/whatsapp` procesando eventos `MESSAGES_UPSERT`, `MESSAGES_UPDATE` y `SEND_MESSAGE` en tiempo real.\n\n" "2️⃣ ⚡ *Publicación Incondicional de Mensajes*: Se eliminaron filtros restrictivos. Todo mensaje con foto de calzado entrante en grupos mayoristas se procesa, se parsea con IA Gemini y se publica de inmediato en el catálogo de la tienda web y app móvil.\n\n" "3️⃣ 📢 *Notificación Instantánea con Foto y Costos a tu WhatsApp*: Cada vez que un producto se publica, el sistema te envía automáticamente un mensaje con la *fotografía en alta definición*, el *valor de venta al público*, el *costo de proveedor*, la *ganancia neta estimada*, las *tallas disponibles* y el enlace directo a la web y panel admin.\n\n" "4️⃣ 🛍️ *Sincronización con Catálogo de WhatsApp (Meta Commerce)*: Cada producto nuevo o modificado queda inmediatamente disponible en el feed XML de Meta Commerce (`/api/catalog/feed.xml`) y sincronizado con el Catálogo de WhatsApp Business.\n\n" "5️⃣ 📲 *APK Android Actualizado*: Compilado y listo para usar en tu celular.\n\n" "👉 *Tienda Web:* https://drip-zone.online\n" "👉 *Panel de Productos:* https://drip-zone.online/admin/productos\n" "📥 *Descarga directa del APK:* https://drip-zone.online/DripZone.apk" ) payload = { "number": "573167455877", "mediatype": "document", "mimetype": "application/vnd.android.package-archive", "caption": caption, "media": "https://drip-zone.online/DripZone.apk", "fileName": "DripZone.apk" } try: req = urllib.request.Request(url, data=json.dumps(payload).encode('utf-8'), headers=headers) with urllib.request.urlopen(req, timeout=180) as resp: print(f"[OK] APK despachada con exito al WhatsApp de Carlos (HTTP {resp.status})") except Exception as e: print(f"[Aviso] No se pudo enviar archivo multimedia por Evolution: {e}. Enviando mensaje de texto...") text_url = "https://evolution.almeira.com.co/message/sendText/AutomataAlmeira" text_payload = { "number": "573167455877", "text": ( f"{caption}\n\n" "📥 *Descarga e instala el APK actualizado aquí:*\n" "👉 https://drip-zone.online/DripZone.apk" ) } try: req2 = urllib.request.Request(text_url, data=json.dumps(text_payload).encode('utf-8'), headers=headers) with urllib.request.urlopen(req2, timeout=20) as resp2: print(f"[OK] Notificacion de texto enviada por WhatsApp (HTTP {resp2.status})") except Exception as e2: print(f"[Error] No se pudo enviar texto por WhatsApp: {e2}") if __name__ == "__main__": deploy()