Files
fn_registry/python/functions/browser/whatsapp_send_message.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

142 lines
5.7 KiB
Python

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