ahora si funciona
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user