"""Abre un chat de WhatsApp Web en una pestana ya logueada via Chrome DevTools Protocol. Compone cuatro primitivas CDP del registry (`cdp_eval`, `cdp_type_chars`, `cdp_press_key`, `cdp_click_xy`) para localizar y abrir un chat por su nombre exacto SIN abrir ventana nueva ni darle foco al sistema: 1. Limpia el buscador (`Escape` no basta: el texto se acumula -> select + Backspace). 2. Enfoca el input de busqueda y teclea el nombre caracter a caracter. 3. Localiza el chat por su ancla estable `span[title=""]` dentro de `#side` y calcula el centro de su bounding box. 4. Hace un click de RATON REAL sobre esas coordenadas (un `element.click()` JS no abre el chat: los handlers de React lo ignoran). 5. Verifica que abrio comprobando que el aria-label del composer contiene el nombre. Base de `whatsapp_read_chat` y `whatsapp_send_message`: ambas necesitan el chat abierto antes de leer o enviar. """ 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.cdp_click_xy import cdp_click_xy def _ev(expr: str, port: int, substr: str) -> dict: """Atajo: evalua una expresion JS en el target de WhatsApp.""" return cdp_eval(expr, port=port, target_url_substr=substr) def whatsapp_open_chat( name: str, *, port: int = 9222, target_url_substr: str = "whatsapp", wait_s: float = 1.3, ) -> dict: """Abre un chat de WhatsApp Web por su nombre exacto en una pestana logueada. Args: name: Nombre EXACTO del chat/grupo tal y como aparece en la lista lateral (match exacto del atributo `title` del `span` ancla). Nombres ambiguos abren el primero que matchee. port: Puerto de remote debugging de Chrome. Default 9222. target_url_substr: Substring que debe contener la URL del target (pestana). Default "whatsapp". wait_s: Segundos de espera tras teclear el nombre para que la lista lateral filtre y renderice los resultados. Default 1.3. Returns: dict con claves: opened: bool — True si el chat se abrio (el nombre aparece en el aria-label del composer). name: str — el nombre solicitado. composer_label: str — aria-label del composer (solo si abrio). reason: str — motivo del fallo (solo si no abrio). coords: dict {x, y} — coordenadas del click (solo si encontro el ancla). """ substr = target_url_substr # 1. Limpiar el buscador. Escape NO basta: el texto se acumula entre llamadas, # asi que seleccionamos todo el contenido del input y lo borramos. cdp_press_key("Escape", port=port, target_url_substr=substr) time.sleep(0.3) _ev( "var i=document.querySelector('#side input'); if(i){i.focus(); i.select();}", port, substr, ) cdp_press_key("Backspace", port=port, target_url_substr=substr) time.sleep(0.2) # 2. Enfocar el input de busqueda y teclear el nombre caracter a caracter. _ev("var i=document.querySelector('#side input'); if(i){i.focus();}", port, substr) time.sleep(0.2) cdp_type_chars(name, port=port, target_url_substr=substr, delay_ms=15) time.sleep(wait_s) # 3. Localizar el ancla estable: span[title] con nombre EXACTO dentro de #side. # Devuelve el centro del bounding box, o un marcador offscreen si no es visible. expr = ( "(() => { const name=" + json.dumps(name) + ";" "const a=[...document.querySelectorAll('#side span[title]')]" ".find(s=>s.getAttribute('title')===name);" "if(!a) return null; const b=a.getBoundingClientRect();" "if(b.width===0||b.y<0) return JSON.stringify({offscreen:true});" "return JSON.stringify({x:Math.round(b.x+b.width/2),y:Math.round(b.y+b.height/2)});})()" ) r = _ev(expr, port, substr) if not r.get("value"): return { "opened": False, "name": name, "reason": "chat no encontrado en la lista (no cargado o nombre inexacto)", } c = json.loads(r["value"]) if c.get("offscreen"): return { "opened": False, "name": name, "reason": "chat fuera de viewport (scroll necesario)", } # 4. Click de raton real sobre el ancla. Un element.click() JS NO abre el chat # porque los handlers de React no reaccionan a eventos sinteticos del DOM. cdp_click_xy(c["x"], c["y"], port=port, target_url_substr=substr) time.sleep(1.1) # 5. Verificar: el composer (footer contenteditable) apunta al chat abierto. chk = _ev( "var b=document.querySelector('footer div[contenteditable=\"true\"]'); " "b?b.getAttribute('aria-label'):null", port, substr, ) label = chk.get("value") or "" return { "opened": name in label, "name": name, "composer_label": label, "coords": c, } if __name__ == "__main__": chat = sys.argv[1] if len(sys.argv) > 1 else "NOTAS WASAP" out = whatsapp_open_chat(chat, port=9222, target_url_substr="whatsapp") print(json.dumps(out, ensure_ascii=False, indent=2))