10bfb846a8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
5.2 KiB
Python
136 lines
5.2 KiB
Python
"""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="<nombre exacto>"]` 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))
|