10bfb846a8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
142 lines
5.7 KiB
Python
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))
|