Files
fn_registry/python/functions/browser/whatsapp_open_chat.py
T
egutierrez 10bfb846a8 ahora si funciona
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 16:23:52 +02:00

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))