diff --git a/src/Backend/WhatsappPromo.Engine/Wapi/JsSnippets.cs b/src/Backend/WhatsappPromo.Engine/Wapi/JsSnippets.cs index be27fca..0afd226 100644 --- a/src/Backend/WhatsappPromo.Engine/Wapi/JsSnippets.cs +++ b/src/Backend/WhatsappPromo.Engine/Wapi/JsSnippets.cs @@ -3,171 +3,172 @@ namespace WhatsappPromo.Engine.Wapi public static class JsSnippets { public const string MediaExtractor = @" - console.log('Injected MediaExtractor initialized'); - - // Set to keep track of processed message IDs - window.processedMessages = new Set(); - - window.scanForMedia = async () => { - // Buscar todos los contenedores de mensajes - const messages = document.querySelectorAll('div[role=""row""]'); + (function() { + var log = function(m) { if (window.onLog) window.onLog(m); }; + log('[WAPP] Injected MediaExtractor initialized'); - for (const msgRow of messages) { - // Chequear si es mensaje entrante (ignorar mensajes enviados) - if (!msgRow.classList.contains('message-in')) continue; + if (!window.processedMessages) window.processedMessages = new Set(); + + var triggerHumanClick = function(el) { + if (!el) return; + try { + el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' }); + var rect = el.getBoundingClientRect(); + var x = rect.left + rect.width / 2; + var y = rect.top + rect.height / 2; + var opt = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y, buttons: 1 }; + + el.dispatchEvent(new MouseEvent('mouseover', opt)); + el.dispatchEvent(new MouseEvent('mousedown', opt)); + el.focus(); + + setTimeout(function() { + el.dispatchEvent(new MouseEvent('mouseup', opt)); + el.dispatchEvent(new MouseEvent('click', opt)); + if (el.click) el.click(); + }, 50); + } catch(e) { log('Error en clic: ' + e.message); } + }; - // Chequear si tiene imagen/video - const img = msgRow.querySelector('img[src^=""blob:""]'); - if (!img) continue; - - const blobUrl = img.src; - - // Usar ID único para evitar duplicados - const msgId = msgRow.getAttribute('data-id') || blobUrl; - if (window.processedMessages.has(msgId)) continue; - - window.processedMessages.add(msgId); - - console.log('Found new media:', blobUrl); - - try { - // EXTRACCIÓN AUTOMÁTICA DE TELÉFONO (múltiples métodos) - let phone = ''; - - // MÉTODO 1: Buscar en atributos data-* del mensaje (más confiable) - // WhatsApp Web usa atributos internos que pueden contener el JID (phone@c.us) - const dataPrePlainText = msgRow.querySelector('[data-pre-plain-text]'); - if (dataPrePlainText && !phone) { - const prePlainText = dataPrePlainText.getAttribute('data-pre-plain-text'); - // Formato típico: ""[HH:MM, DD/MM/YYYY] Nombre: "" - // Pero en algunos casos incluye el número - const match = prePlainText.match(/(\d{10,15})/); - if (match) { - phone = match[1]; - console.log('Phone extracted from data-pre-plain-text:', phone); - } - } - - // MÉTODO 2: Buscar en el elemento padre que podría tener data-id con formato phone@c.us - if (!phone) { - let parentEl = msgRow; - for (let i = 0; i < 5; i++) { // Subir hasta 5 niveles - if (!parentEl) break; - const dataId = parentEl.getAttribute('data-id'); - if (dataId && dataId.includes('@')) { - // Formato: ""false_5491112345678@c.us_MESSAGEID"" - const parts = dataId.split('_'); - for (const part of parts) { - if (part.includes('@c.us')) { - const phoneMatch = part.match(/(\d{10,15})@/); - if (phoneMatch) { - phone = phoneMatch[1]; - console.log('Phone extracted from data-id:', phone); - break; - } - } - } - if (phone) break; - } - parentEl = parentEl.parentElement; - } - } - - // MÉTODO 3: Buscar en elementos cercanos con atributos title o aria-label - if (!phone) { - const titleElements = msgRow.querySelectorAll('[title], [aria-label]'); - for (const el of titleElements) { - const text = el.getAttribute('title') || el.getAttribute('aria-label') || ''; - const phoneMatch = text.match(/\+?(\d{10,15})/); - if (phoneMatch && phoneMatch[1].length >= 10) { - phone = phoneMatch[1].replace(/\D/g, ''); - console.log('Phone extracted from title/aria-label:', phone); - break; - } - } - } - - // MÉTODO 4 (FALLBACK): Header del chat abierto (requiere intervención humana) - // Solo se usa si los métodos anteriores fallaron - if (!phone) { - const headerTitle = document.querySelector('header span[title]'); - if (headerTitle) { - phone = headerTitle.getAttribute('title').replace(/\D/g, ''); - console.log('Phone extracted from header (fallback):', phone); - } - } - - // MÉTODO 5 (ÚLTIMO RECURSO): Buscar el chat activo en la lista lateral - if (!phone) { - const activeChat = document.querySelector('div[aria-selected=""true""]'); - if (activeChat) { - const chatTitle = activeChat.querySelector('span[title]'); - if (chatTitle) { - const titleText = chatTitle.getAttribute('title'); - const phoneMatch = titleText.match(/\+?(\d{10,15})/); - if (phoneMatch) { - phone = phoneMatch[1].replace(/\D/g, ''); - console.log('Phone extracted from active chat:', phone); - } - } - } - } - - // VALIDACIÓN FINAL - if (!phone || phone.length < 8) { - console.warn('❌ No se pudo detectar el teléfono con ningún método. Mensaje ignorado.'); - console.warn('💡 Sugerencia: Asegúrate de tener WhatsApp Web completamente cargado.'); - // NO llamar a onMediaDownloaded, simplemente ignorar este mensaje - // y continuar con el siguiente - continue; - } - - console.log('✅ Teléfono detectado exitosamente:', phone); - - // Simular pequeño retraso random antes de descargar - await new Promise(r => setTimeout(r, Math.random() * 2000 + 500)); - - const response = await fetch(blobUrl); - const blob = await response.blob(); - const reader = new FileReader(); - - reader.readAsDataURL(blob); - reader.onloadend = () => { - const base64data = reader.result.split(',')[1]; - const mimeType = blob.type; - - // Enviar a C# - window.onMediaDownloaded(phone, base64data, mimeType); - }; - } catch (err) { - console.error('Error downloading media:', err); - } - } - }; - - // Anti-detección: Usar MutationObserver en lugar de polling constante - const observer = new MutationObserver((mutations) => { - let shouldScan = false; - for(const mutation of mutations) { - if (mutation.addedNodes.length > 0) { - shouldScan = true; - break; + window.getIdentityStrict = function() { + // El usuario nos dio el selector exacto: span[data-testid=""selectable-text""] + // Buscamos cualquier elemento con ese ID que contenga un ""+"" + var potentialNumbers = Array.from(document.querySelectorAll('span[data-testid=""selectable-text""]')); + + for (var i = 0; i < potentialNumbers.length; i++) { + var el = potentialNumbers[i]; + var text = (el.textContent || '').trim(); + + // Solo tomamos números que empiecen con + y tengan longitud válida + if (text.startsWith('+') && text.replace(/\D/g, '').length >= 10) { + // IMPORTANTE: El panel lateral siempre está a la derecha (más del 50% del ancho) + var rect = el.getBoundingClientRect(); + if (rect.left > (window.innerWidth / 2)) { + return text.replace(/[^\d+]/g, ''); + } + } } - } - - if (shouldScan) { - // Debounce ligero - if (window.scanTimeout) clearTimeout(window.scanTimeout); - window.scanTimeout = setTimeout(window.scanForMedia, 1000); - } - }); - - // Conectar al contenedor principal de la app - const appRoot = document.getElementById('app') || document.body; - observer.observe(appRoot, { childList: true, subtree: true }); - // Fallback: ejecutar scan ocasionalmente por si acaso - setInterval(window.scanForMedia, 10000); + // Si llegamos aquí, el número no se encontró en la parte derecha + // Intentamos abrir el panel clicando en la cabecera del chat + var header = document.querySelector('#main header'); + if (header) { + log('[WAPP] No se detecta número en la derecha. Intentando abrir/recargar panel lateral...'); + // Clicamos en la zona del nombre/avatar + var clickArea = header.querySelector('div[role=""button""]') || + header.querySelector('div._akz_') || + header; + triggerHumanClick(clickArea); + } + + return null; + }; + + window.openUnreadChats = async function() { + var markers = Array.from(document.querySelectorAll('span[aria-label]')).filter(function(s) { + var lb = (s.getAttribute('aria-label') || '').toLowerCase(); + return (lb.indexOf('unread') !== -1 || lb.indexOf('leído') !== -1 || lb.indexOf('leido') !== -1) && /^\d+$/.test(s.textContent.trim()); + }); + + if (markers.length > 0) { + for (var i = 0; i < markers.length; i++) { + try { + var marker = markers[i]; + var row = marker.closest('div[role=""listitem""]') || + marker.closest('div[role=""gridcell""]') || + marker.closest('[data-testid^=""list-item""]'); + + if (!row || row.getAttribute('aria-selected') === 'true' || row.querySelector('[aria-selected=""true""]')) continue; + + log('[WAPP] >>> Navegando a chat pendiente...'); + var target = row.querySelector('div[data-testid=""cell-frame-container""]') || row; + triggerHumanClick(target); + await new Promise(r => setTimeout(r, 6000)); + break; + } catch (e) { log('Error nav: ' + e.message); } + } + } + }; + + window.scanForMedia = async function() { + // 1. Descargas pendientes (kB/MB) + var btns = document.querySelectorAll('div[role=""button""]'); + for (var i = 0; i < btns.length; i++) { + var text = btns[i].textContent || ''; + if (text.match(/\d+ (kB|MB)/) && !btns[i].getAttribute('data-scan-clicked')) { + log('[WAPP] Forzando clic de descarga: ' + text); + btns[i].setAttribute('data-scan-clicked', 'true'); + triggerHumanClick(btns[i]); + return; + } + } + + // 2. Identificación ESTRICTA (Solo derecha de la pantalla) + var iden = window.getIdentityStrict(); + if (!iden) { + // Espera más larga para que el panel cargue número (+54...) + await new Promise(r => setTimeout(r, 5000)); + iden = window.getIdentityStrict(); + } + + if (!iden) { + console.error('[WAPP] ERROR CRÍTICO: No se encontró número con ""+"" en el panel lateral derecho. Abortando guardado.'); + return; + } + + // 3. Captura de contenido + var messages = document.querySelectorAll('.message-in, [data-testid=""msg-container""]'); + for (var j = 0; j < messages.length; j++) { + var msg = messages[j]; + var msgId = msg.getAttribute('data-id') || msg.closest('[data-id]')?.getAttribute('data-id') || ''; + var contents = msg.querySelectorAll('img[src^=""blob:""], video[src^=""blob:""]'); + + for (var k = 0; k < contents.length; k++) { + var media = contents[k]; + var url = media.src || media.getAttribute('src'); + if (!url || !url.startsWith('blob:')) continue; + + var key = msgId + '_' + k; + if (window.processedMessages.has(key)) continue; + window.processedMessages.add(key); + + log('[WAPP] Número validado: ' + iden + '. Guardando archivo...'); + + try { + await new Promise(r => setTimeout(r, 2000)); + var res = await fetch(url); + var b = await res.blob(); + var rd = new FileReader(); + rd.readAsDataURL(b); + rd.onloadend = function() { + var b64 = rd.result.split(',')[1]; + window.onMediaDownloaded(iden, b64, b.type); + }; + } catch (err) { log('Error de guardado: ' + err.message); } + } + } + }; + + var obs = new MutationObserver(function(muts) { + var hasNew = false; + for(var m=0; m 0) { hasNew = true; break; } } + if (hasNew) { + if (window.st) clearTimeout(window.st); + window.st = setTimeout(window.scanForMedia, 2000); + } + }); + + var startMonitor = function() { + var target = document.getElementById('app') || document.body; + if (!target) { setTimeout(startMonitor, 1000); return; } + obs.observe(target, { childList: true, subtree: true }); + log('[WAPP] Monitor activo'); + }; + + startMonitor(); + setInterval(window.scanForMedia, 14000); + setInterval(window.openUnreadChats, 11000); + })(); "; } } diff --git a/src/Backend/WhatsappPromo.Worker/WhatsappWorker.cs b/src/Backend/WhatsappPromo.Worker/WhatsappWorker.cs index 964f233..a61772a 100644 --- a/src/Backend/WhatsappPromo.Worker/WhatsappWorker.cs +++ b/src/Backend/WhatsappPromo.Worker/WhatsappWorker.cs @@ -85,6 +85,11 @@ namespace WhatsappPromo.Worker var page = await _browserService.GetPageAsync(); // Configurar bindings + await page.ExposeFunctionAsync("onLog", (Func)(async (string msg) => + { + await SafeSendAsync("LogUpdate", $"[DEBUG] {msg}"); + })); + await page.ExposeFunctionAsync("onNewMessage", (Func)(async (string jsonMessage) => { await SafeSendAsync("LogUpdate", $"Mensaje recibido: {jsonMessage}"); @@ -113,7 +118,7 @@ namespace WhatsappPromo.Worker }, stoppingToken); })); - await page.EvaluateFunctionOnNewDocumentAsync(JsSnippets.MediaExtractor); + await page.EvaluateExpressionOnNewDocumentAsync(JsSnippets.MediaExtractor); await page.ReloadAsync(); // Procesamiento en segundo plano