"""Envia un mensaje de texto a un chat de WhatsApp Web via Chrome DevTools Protocol. Compone `whatsapp_open_chat` (abrir + localizar el chat por nombre) con tres primitivas CDP del registry (`cdp_eval`, `cdp_type_chars`, `cdp_press_key`) para enviar un texto a un contacto/grupo SIN abrir ventana nueva ni darle foco al sistema. Flujo, con dos salvaguardas anti-envio-al-contacto-equivocado: 1. Abre el chat por su nombre exacto (`open_first=True`). Si no abre, aborta. Con `open_first=False`, asume el chat ya abierto pero VERIFICA que el aria-label del composer contiene el nombre; si no, aborta por seguridad. 2. Enfoca el composer (`footer div[contenteditable="true"]`) y teclea el texto con teclado CDP real (`cdp_type_chars`). NO se usa `execCommand`/`el.value`: el editor Lexical de WhatsApp los ignora y produce texto duplicado/intercalado. 3. Re-lee el `innerText` del composer y comprueba que coincide EXACTAMENTE con el texto pedido antes de enviar. Si no coincide, aborta sin pulsar Enter. 4. Pulsa `Enter` para enviar y devuelve la ultima fila renderizada de `#main`. Validado contra WhatsApp Web real. Base para automatizar el envio de mensajes sobre el navegador diario sin robar el foco al usuario. """ import json import os import sys import time sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from browser.cdp_eval import cdp_eval from browser.cdp_type_chars import cdp_type_chars from browser.cdp_press_key import cdp_press_key from browser.whatsapp_open_chat import whatsapp_open_chat def whatsapp_send_message( name: str, text: str, *, port: int = 9222, target_url_substr: str = "whatsapp", open_first: bool = True, ) -> dict: """Envia un mensaje de texto a un chat de WhatsApp Web en una pestana logueada. Accion CON EFECTO: envia un mensaje DE VERDAD (no reversible). Verifica `name`. Args: name: Nombre EXACTO del chat/grupo destinatario, tal y como aparece en la lista lateral. Se usa para abrir el chat y como salvaguarda de que el composer apunta al destinatario correcto antes de escribir. text: Texto a enviar. Se teclea con teclado CDP real caracter a caracter. `Enter` lo envia (no inserta salto de linea); multilinea no soportado. port: Puerto de remote debugging de Chrome. Default 9222. target_url_substr: Substring que debe contener la URL del target (pestana). Default "whatsapp". open_first: Si True (default), abre el chat por su nombre antes de enviar. Si False, asume el chat ya abierto pero verifica el aria-label del composer contra `name` antes de escribir (aborta si no coincide). Returns: dict con claves: sent: bool — True si el mensaje se envio. name: str — el nombre solicitado. last_row: str — texto de la ultima fila renderizada de #main tras enviar (solo si sent=True). reason: str — motivo del fallo (solo si sent=False). composer: str — contenido real del composer cuando hubo mismatch (solo si sent=False por texto inesperado). Nunca lanza: los fallos se reportan en "sent" + "reason". """ S = target_url_substr # 1. Abrir + verificar destinatario correcto (salvaguarda anti-equivocacion). if open_first: o = whatsapp_open_chat(name, port=port, target_url_substr=S) if not o.get("opened"): return { "sent": False, "name": name, "reason": o.get("reason", "no se pudo abrir el chat"), } else: chk = cdp_eval( "var b=document.querySelector('footer div[contenteditable=\"true\"]'); " "b?b.getAttribute('aria-label'):null", port=port, target_url_substr=S, ) if name not in (chk.get("value") or ""): return { "sent": False, "name": name, "reason": "el chat abierto no coincide con el destinatario; abortado por seguridad", } # 2. Enfocar el composer y escribir con teclado real (NO execCommand: rompe Lexical). cdp_eval( "var b=document.querySelector('footer div[contenteditable=\"true\"]'); " "if(b){b.focus();}", port=port, target_url_substr=S, ) time.sleep(0.25) cdp_type_chars(text, port=port, target_url_substr=S, delay_ms=15) time.sleep(0.3) # 3. Verificar que el composer tiene EXACTAMENTE el texto antes de enviar. chk = cdp_eval( "var b=document.querySelector('footer div[contenteditable=\"true\"]'); " "b?b.innerText.replace(/\\n/g,''):''", port=port, target_url_substr=S, ) composer = chk.get("value") or "" if composer != text: return { "sent": False, "name": name, "reason": "el composer no contiene el texto esperado (no enviado)", "composer": composer, } # 4. Enviar (Enter) y confirmar leyendo la ultima fila de #main. cdp_press_key("Enter", port=port, target_url_substr=S) time.sleep(0.7) last = cdp_eval( "var r=[...document.querySelectorAll('#main [role=\"row\"]')].slice(-1)[0]; " "r?r.innerText.replace(/\\s+/g,' ').trim().slice(0,200):null", port=port, target_url_substr=S, ) return {"sent": True, "name": name, "last_row": last.get("value")} if __name__ == "__main__": chat = sys.argv[1] if len(sys.argv) > 1 else "NOTAS WASAP" msg = sys.argv[2] if len(sys.argv) > 2 else "hola desde el registry" out = whatsapp_send_message(chat, msg, port=9222, target_url_substr="whatsapp") print(json.dumps(out, ensure_ascii=False, indent=2))