feat: Implement main application shell with navigation and color scheme toggle
- Added Appshell component with responsive navbar and main content area - Integrated ColorSchemeToggle for light/dark mode switching - Created Welcome component with styled title and introductory text - Developed ChatPage for LLM interaction with WebSocket support - Implemented Biblioteca for managing notes with rich text editor - Added LoginPage for user authentication with error handling - Introduced MessageList and MessageBubble components for chat messages - Styled components with CSS modules for consistent design
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
import random
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .Tab import Tab
|
||||
|
||||
class ElementoWeb:
|
||||
def __init__(self, tab: "Tab", object_id: Optional[str]):
|
||||
self.tab = tab
|
||||
self.object_id = object_id
|
||||
self._node_id = None # Lazy resolved
|
||||
|
||||
@classmethod
|
||||
def from_node(cls, tab: "Tab", node_id: int) -> "ElementoWeb":
|
||||
inst = cls(tab, object_id=None)
|
||||
inst._node_id = node_id
|
||||
return inst
|
||||
|
||||
async def _asegurar_object_id(self):
|
||||
if not self.object_id and self._node_id:
|
||||
try:
|
||||
resolved = await self.tab._enviar("DOM.resolveNode", {"nodeId": self._node_id})
|
||||
self.object_id = resolved["object"]["objectId"]
|
||||
except Exception as e:
|
||||
print(f"⚠️ No se pudo resolver objectId desde nodeId: {e}")
|
||||
|
||||
async def scroll_into_view(self):
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": "function() { this.scrollIntoView({block: 'center'}); }",
|
||||
"awaitPromise": True
|
||||
})
|
||||
if self.tab.verbose:
|
||||
print("📜 Elemento desplazado a la vista.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al hacer scroll hacia el elemento: {e}")
|
||||
|
||||
async def click(self):
|
||||
try:
|
||||
await self.scroll_into_view()
|
||||
await self._asegurar_object_id()
|
||||
if not self.object_id:
|
||||
raise ValueError("No se puede obtener objectId del elemento para hacer click.")
|
||||
|
||||
# Intenta obtener coordenadas del nodo
|
||||
node_result = await self.tab._enviar("DOM.describeNode", {
|
||||
"objectId": self.object_id
|
||||
})
|
||||
node_id = node_result["node"]["nodeId"]
|
||||
|
||||
try:
|
||||
box_model = await self.tab._enviar("DOM.getBoxModel", {"nodeId": node_id})
|
||||
content = box_model["model"]["content"]
|
||||
x = (content[0] + content[2]) / 2
|
||||
y = (content[1] + content[5]) / 2
|
||||
except:
|
||||
quads_result = await self.tab._enviar("DOM.getContentQuads", {"nodeId": node_id})
|
||||
quad = quads_result["quads"][0]
|
||||
x = (quad[0] + quad[4]) / 2
|
||||
y = (quad[1] + quad[5]) / 2
|
||||
|
||||
# 🧠 Enfocar el elemento antes de clickear
|
||||
await self.tab._enviar("DOM.focus", {
|
||||
"objectId": self.object_id
|
||||
})
|
||||
|
||||
# 🎯 Movimiento humanoide opcional
|
||||
start_x, start_y = x + random.uniform(-100, 100), y + random.uniform(-100, 100)
|
||||
steps = random.randint(5, 12)
|
||||
for i in range(1, steps + 1):
|
||||
curr_x = start_x + (x - start_x) * i / steps + random.uniform(-1, 1)
|
||||
curr_y = start_y + (y - start_y) * i / steps + random.uniform(-1, 1)
|
||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||
"type": "mouseMoved",
|
||||
"x": curr_x,
|
||||
"y": curr_y,
|
||||
})
|
||||
await asyncio.sleep(random.uniform(0.01, 0.05))
|
||||
|
||||
# 👆 Mouse Down
|
||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||
"type": "mousePressed",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"button": "left",
|
||||
"clickCount": 1
|
||||
})
|
||||
|
||||
await asyncio.sleep(random.uniform(0.05, 0.15))
|
||||
|
||||
# 👇 Mouse Up
|
||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||
"type": "mouseReleased",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"button": "left",
|
||||
"clickCount": 1
|
||||
})
|
||||
|
||||
await asyncio.sleep(random.uniform(0.01, 0.05))
|
||||
|
||||
# 🖱️ Click manual adicional
|
||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||
"type": "mouseClicked",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"button": "left",
|
||||
"clickCount": 1
|
||||
})
|
||||
|
||||
if self.tab.verbose:
|
||||
print(f"🖱️ Click humano simulado en ({x:.1f}, {y:.1f})")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al hacer click físico: {e}")
|
||||
print("🧪 Intentando fallback con JavaScript click()...")
|
||||
await self.click_js()
|
||||
|
||||
async def click_js(self):
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
if not self.object_id:
|
||||
print("⚠️ No se puede hacer click JS: objectId no disponible.")
|
||||
return
|
||||
await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": "function() { this.click(); }",
|
||||
"awaitPromise": True
|
||||
})
|
||||
if self.tab.verbose:
|
||||
print("🖱️ Click simulado por JavaScript (element.click())")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al ejecutar click en JS: {e}")
|
||||
|
||||
async def obtener_texto(self) -> Optional[str]:
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
result = await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": "function() { return this.textContent; }",
|
||||
"returnByValue": True
|
||||
})
|
||||
return result.get("result", {}).get("value")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al obtener texto del elemento: {e}")
|
||||
return None
|
||||
|
||||
async def escribir_texto(self, texto: str):
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": f"function() {{ this.value = {json.dumps(texto)}; this.dispatchEvent(new Event('input')); }}",
|
||||
"awaitPromise": True
|
||||
})
|
||||
if self.tab.verbose:
|
||||
print(f"⌨️ Texto escrito en elemento: '{texto}'")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al escribir texto: {e}")
|
||||
|
||||
|
||||
async def encontrar_hijo_clickeable(self) -> Optional["ElementoWeb"]:
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
resultado = await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": """
|
||||
function() {
|
||||
const candidatos = this.querySelectorAll("span, div, a, button");
|
||||
for (const el of candidatos) {
|
||||
const style = window.getComputedStyle(el);
|
||||
const visible = style.display !== "none" && style.visibility !== "hidden";
|
||||
const interactivo = style.pointerEvents !== "none";
|
||||
if (visible && interactivo) return el;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
""",
|
||||
"returnByValue": False
|
||||
})
|
||||
if "result" in resultado and "objectId" in resultado["result"]:
|
||||
return ElementoWeb(self.tab, resultado["result"]["objectId"])
|
||||
except Exception as e:
|
||||
print(f"⚠️ No se pudo encontrar hijo clickeable: {e}")
|
||||
return None
|
||||
@@ -0,0 +1,193 @@
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import json
|
||||
from typing import Optional
|
||||
import aiohttp
|
||||
|
||||
|
||||
class Navegador:
|
||||
def __init__(self,
|
||||
chrome_path: str,
|
||||
user_data_dir: str,
|
||||
id: Optional[int] = None,
|
||||
download_dir: Optional[str] = None,
|
||||
debugging_port: int = 9222,
|
||||
headless: bool = False,
|
||||
user_agent: Optional[str] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"):
|
||||
self.chrome_path = chrome_path
|
||||
self.user_data_dir = user_data_dir
|
||||
self.id = id
|
||||
self.download_dir = download_dir or os.path.join(self.user_data_dir, "downloads")
|
||||
self.debugging_port = debugging_port
|
||||
self.headless = headless
|
||||
self.user_agent = user_agent
|
||||
self.chrome_process: Optional[subprocess.Popen] = None
|
||||
|
||||
async def _esperar_debugger(self, timeout=10):
|
||||
url = f"http://127.0.0.1:{self.debugging_port}/json"
|
||||
for _ in range(timeout * 10): # 10 intentos por segundo
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
print("✅ Chrome listo para debugging.")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(0.1)
|
||||
raise RuntimeError("❌ Chrome no respondió en el puerto de debugging.")
|
||||
|
||||
def _preconfigurar_preferencias(self):
|
||||
prefs_path = os.path.join(self.user_data_dir, "Default", "Preferences")
|
||||
os.makedirs(os.path.dirname(prefs_path), exist_ok=True)
|
||||
os.makedirs(self.download_dir, exist_ok=True)
|
||||
|
||||
prefs = {
|
||||
"profile": {
|
||||
"exit_type": "Normal",
|
||||
"exited_cleanly": True
|
||||
},
|
||||
"browser": {
|
||||
"has_seen_welcome_page": True
|
||||
},
|
||||
"distribution": {
|
||||
"skip_first_run_ui": True
|
||||
},
|
||||
"download": {
|
||||
"default_directory": self.download_dir,
|
||||
"prompt_for_download": False,
|
||||
"directory_upgrade": True,
|
||||
"extensions_to_open": ""
|
||||
},
|
||||
"savefile": {
|
||||
"default_directory": self.download_dir
|
||||
}
|
||||
}
|
||||
|
||||
if os.path.exists(prefs_path):
|
||||
try:
|
||||
with open(prefs_path, "r", encoding="utf-8") as f:
|
||||
existing = json.load(f)
|
||||
existing.update(prefs)
|
||||
prefs = existing
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with open(prefs_path, "w", encoding="utf-8") as f:
|
||||
json.dump(prefs, f, indent=2)
|
||||
|
||||
def _build_args(self):
|
||||
os.makedirs(self.user_data_dir, exist_ok=True)
|
||||
self._preconfigurar_preferencias()
|
||||
|
||||
args = [
|
||||
f"--remote-debugging-port={self.debugging_port}",
|
||||
f"--user-data-dir={self.user_data_dir}",
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
"--no-sandbox",
|
||||
# "--disable-web-security",
|
||||
# "--disable-extensions",
|
||||
# "--disable-dev-shm-usage",
|
||||
"--disable-infobars",
|
||||
"--disable-popup-blocking",
|
||||
"--disable-default-apps",
|
||||
"--mute-audio",
|
||||
"--window-size=1024,1024",
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--disable-features=DefaultBrowserPrompt",
|
||||
"--disable-component-update",
|
||||
"--disable-background-networking",
|
||||
"--disable-sync",
|
||||
"--disable-translate",
|
||||
"--disable-background-timer-throttling",
|
||||
"--disable-client-side-phishing-detection",
|
||||
"--disable-component-extensions-with-background-pages",
|
||||
"--metrics-recording-only",
|
||||
"--safebrowsing-disable-auto-update",
|
||||
|
||||
|
||||
]
|
||||
|
||||
if self.headless:
|
||||
args.append("--headless=new")
|
||||
|
||||
if self.user_agent:
|
||||
args.append(f"--user-agent={self.user_agent}")
|
||||
|
||||
return args
|
||||
|
||||
|
||||
|
||||
async def inyectar_spoof_chrome(self):
|
||||
script = """
|
||||
window.chrome = {
|
||||
app: {
|
||||
isInstalled: false,
|
||||
InstallState: {
|
||||
DISABLED: 'disabled',
|
||||
INSTALLED: 'installed',
|
||||
NOT_INSTALLED: 'not_installed'
|
||||
},
|
||||
RunningState: {
|
||||
CANNOT_RUN: 'cannot_run',
|
||||
READY_TO_RUN: 'ready_to_run',
|
||||
RUNNING: 'running'
|
||||
}
|
||||
},
|
||||
runtime: {
|
||||
PlatformOs: { MAC: 'mac', WIN: 'win', ANDROID: 'android', CROS: 'cros', LINUX: 'linux', OPENBSD: 'openbsd' },
|
||||
PlatformArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
|
||||
PlatformNaclArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
|
||||
RequestUpdateCheckStatus: { THROTTLED: 'throttled', NO_UPDATE: 'no_update', UPDATE_AVAILABLE: 'update_available' },
|
||||
OnInstalledReason: { INSTALL: 'install', UPDATE: 'update', CHROME_UPDATE: 'chrome_update', SHARED_MODULE_UPDATE: 'shared_module_update' },
|
||||
OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' }
|
||||
}
|
||||
};
|
||||
"""
|
||||
|
||||
url = f"http://127.0.0.1:{self.debugging_port}/json"
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
targets = await resp.json()
|
||||
|
||||
for target in targets:
|
||||
if "webSocketDebuggerUrl" not in target:
|
||||
continue
|
||||
|
||||
target_id = target["id"]
|
||||
async with session.post(
|
||||
f"http://127.0.0.1:{self.debugging_port}/json/protocol",
|
||||
json={"targetId": target_id}
|
||||
):
|
||||
pass # CDP protocol fetch optional
|
||||
|
||||
async with session.post(
|
||||
f"http://127.0.0.1:{self.debugging_port}/json/send",
|
||||
json={
|
||||
"id": 1,
|
||||
"method": "Page.addScriptToEvaluateOnNewDocument",
|
||||
"params": {"source": script}
|
||||
}
|
||||
) as inject_resp:
|
||||
if inject_resp.status == 200:
|
||||
print("✅ chrome.* spoof inyectado.")
|
||||
|
||||
|
||||
async def iniciar(self):
|
||||
args = self._build_args()
|
||||
self.chrome_process = subprocess.Popen([self.chrome_path] + args)
|
||||
print(f"Chrome iniciado (headless={self.headless}). Esperando disponibilidad del debugger...")
|
||||
await self._esperar_debugger()
|
||||
await self.inyectar_spoof_chrome()
|
||||
|
||||
async def cerrar(self):
|
||||
if self.chrome_process and self.chrome_process.poll() is None:
|
||||
self.chrome_process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(asyncio.to_thread(self.chrome_process.wait), timeout=5)
|
||||
except asyncio.TimeoutError:
|
||||
self.chrome_process.kill()
|
||||
print("🛑 Chrome cerrado correctamente.")
|
||||
@@ -0,0 +1,138 @@
|
||||
import aiohttp
|
||||
import websockets
|
||||
import json
|
||||
import asyncio
|
||||
from .Tab import Tab
|
||||
from typing import Optional
|
||||
|
||||
|
||||
|
||||
class Scrapper:
|
||||
def __init__(self, debugging_url: str = "http://127.0.0.1:9222"):
|
||||
self.debugging_url = debugging_url
|
||||
self.tabs: list[Tab] = []
|
||||
|
||||
async def _crear_tab_websocket_url(self, target_url: str = "about:blank") -> str:
|
||||
"""
|
||||
Crea una nueva pestaña usando el método oficial Target.createTarget
|
||||
y devuelve su WebSocketDebuggerUrl.
|
||||
"""
|
||||
# 1. Obtener el WebSocket general del browser (root)
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{self.debugging_url}/json/version") as resp:
|
||||
if resp.status != 200:
|
||||
raise RuntimeError("No se pudo obtener información del navegador")
|
||||
data = await resp.json()
|
||||
browser_ws_url = data["webSocketDebuggerUrl"]
|
||||
|
||||
# 2. Conectarse al WebSocket del browser
|
||||
async with websockets.connect(browser_ws_url) as websocket:
|
||||
# 3. Enviar comando para crear target
|
||||
msg_id = 1
|
||||
await websocket.send(json.dumps({
|
||||
"id": msg_id,
|
||||
"method": "Target.createTarget",
|
||||
"params": {
|
||||
"url": target_url,
|
||||
"newWindow": False
|
||||
}
|
||||
}))
|
||||
|
||||
# 4. Esperar respuesta con el targetId
|
||||
while True:
|
||||
respuesta = await websocket.recv()
|
||||
data = json.loads(respuesta)
|
||||
if data.get("id") == msg_id:
|
||||
target_id = data["result"]["targetId"]
|
||||
break
|
||||
|
||||
# 5. Esperar a que el target aparezca en /json
|
||||
for _ in range(30): # máximo ~3 segundos
|
||||
await asyncio.sleep(0.1)
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{self.debugging_url}/json") as resp:
|
||||
if resp.status == 200:
|
||||
tabs = await resp.json()
|
||||
for tab in tabs:
|
||||
if tab.get("id") == target_id:
|
||||
return tab["webSocketDebuggerUrl"]
|
||||
|
||||
raise RuntimeError("No se pudo obtener el WebSocket de la nueva pestaña")
|
||||
|
||||
async def nueva_tab(self, url: str = "", wait_time: float = 5.0) -> Tab:
|
||||
websocket_url = await self._crear_tab_websocket_url()
|
||||
tab = await Tab.crear_desde_websocket(websocket_url)
|
||||
self.tabs.append(tab)
|
||||
|
||||
if url:
|
||||
print(f"🌍 Navegando a: {url}")
|
||||
await tab.navegar(url, wait_time)
|
||||
else:
|
||||
print("⚠️ No se especificó URL. La pestaña se creó pero no se navegó a ninguna página.")
|
||||
|
||||
return tab
|
||||
|
||||
async def cerrar_todos(self):
|
||||
for tab in list(self.tabs):
|
||||
await tab.cerrar()
|
||||
self.tabs.clear()
|
||||
|
||||
def get_tab(self, identifier: str) -> Optional[Tab]:
|
||||
"""
|
||||
Devuelve una instancia de Tab según su WebSocket URL o su ID final (extraído del WebSocket URL).
|
||||
Acepta:
|
||||
- ws_url completo: ws://127.0.0.1:9222/devtools/page/XYZ
|
||||
- id directo: XYZ
|
||||
"""
|
||||
for tab in self.tabs:
|
||||
# Comparar directamente contra ws_url
|
||||
if tab.ws_url == identifier:
|
||||
return tab
|
||||
|
||||
# Comparar contra el ID extraído
|
||||
ws_id = tab.ws_url.rsplit("/", 1)[-1]
|
||||
if ws_id == identifier:
|
||||
return tab
|
||||
|
||||
return None
|
||||
|
||||
async def obtener_tabs_existentes(self) -> list[Tab]:
|
||||
"""
|
||||
Recupera todas las pestañas de tipo 'page' que no están ya en self.tabs,
|
||||
las conecta y devuelve como lista. Muestra resumen limpio por consola.
|
||||
"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{self.debugging_url}/json") as resp:
|
||||
if resp.status != 200:
|
||||
raise RuntimeError("No se pudo obtener la lista de pestañas")
|
||||
|
||||
tabs_info = await resp.json()
|
||||
|
||||
print("\n🧾 Pestañas activas (filtradas: solo 'type': 'page'):\n")
|
||||
nuevas_tabs = []
|
||||
for idx, tab_info in enumerate(tabs_info, start=1):
|
||||
tipo = tab_info.get("type")
|
||||
if tipo != "page":
|
||||
continue # Filtrar todo lo que no sea página visible
|
||||
|
||||
ws_url = tab_info.get("webSocketDebuggerUrl")
|
||||
tab_id = tab_info.get("id")
|
||||
title = tab_info.get("title", "<Sin título>")
|
||||
url = tab_info.get("url", "<Sin URL>")
|
||||
|
||||
# Verifica si ya la tienes cargada
|
||||
if any(t.ws_url == ws_url for t in self.tabs):
|
||||
continue
|
||||
|
||||
# Conectar
|
||||
try:
|
||||
tab = await Tab.crear_desde_websocket(ws_url)
|
||||
self.tabs.append(tab)
|
||||
nuevas_tabs.append(tab)
|
||||
except Exception as e:
|
||||
print(f"⚠️ No se pudo conectar a pestaña {tab_id}: {e}")
|
||||
|
||||
if not nuevas_tabs:
|
||||
print("⚠️ No se encontraron nuevas pestañas para agregar.\n")
|
||||
|
||||
return nuevas_tabs
|
||||
@@ -0,0 +1,206 @@
|
||||
import asyncio
|
||||
import json
|
||||
import base64
|
||||
import websockets
|
||||
from typing import Optional, List
|
||||
from .ElementoWeb import ElementoWeb
|
||||
import os
|
||||
|
||||
|
||||
class Tab:
|
||||
def __init__(self, websocket: websockets.WebSocketClientProtocol, ws_url: str, verbose: bool = True):
|
||||
self.websocket = websocket
|
||||
self.ws_url = ws_url
|
||||
self._message_id = 0
|
||||
self._pending = {}
|
||||
self._load_event = asyncio.Event()
|
||||
self.verbose = verbose
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
await self.cerrar()
|
||||
|
||||
@classmethod
|
||||
async def crear_desde_websocket(cls, ws_url: str) -> "Tab":
|
||||
websocket = await websockets.connect(ws_url, max_size=10 * 1024 * 1024)
|
||||
tab = cls(websocket, ws_url)
|
||||
asyncio.create_task(tab._recibir_eventos())
|
||||
await tab._enviar("Page.enable")
|
||||
await tab._enviar("Network.enable")
|
||||
return tab
|
||||
|
||||
async def _recibir_eventos(self):
|
||||
async for mensaje in self.websocket:
|
||||
data = json.loads(mensaje)
|
||||
if "id" in data and data["id"] in self._pending:
|
||||
future = self._pending.pop(data["id"])
|
||||
if "result" in data:
|
||||
future.set_result(data["result"])
|
||||
elif "error" in data:
|
||||
future.set_exception(Exception(data["error"]))
|
||||
elif data.get("method") == "Page.loadEventFired":
|
||||
self._load_event.set()
|
||||
|
||||
async def _enviar(self, metodo: str, parametros: Optional[dict] = None, timeout: float = 10.0) -> dict:
|
||||
self._message_id += 1
|
||||
msg_id = self._message_id
|
||||
mensaje = {
|
||||
"id": msg_id,
|
||||
"method": metodo,
|
||||
"params": parametros or {}
|
||||
}
|
||||
|
||||
future = asyncio.get_event_loop().create_future()
|
||||
self._pending[msg_id] = future
|
||||
await self.websocket.send(json.dumps(mensaje))
|
||||
return await asyncio.wait_for(future, timeout=timeout)
|
||||
|
||||
async def navegar(self, url: str, wait_time: float = 5.0):
|
||||
self._load_event.clear()
|
||||
if self.verbose:
|
||||
print(f"🌍 Navegando a: {url}")
|
||||
await self._enviar("Page.navigate", {"url": url})
|
||||
try:
|
||||
await asyncio.wait_for(self._load_event.wait(), timeout=wait_time)
|
||||
if self.verbose:
|
||||
print("✅ Página cargada correctamente.")
|
||||
except asyncio.TimeoutError:
|
||||
print(f"⚠️ Tiempo de espera agotado ({wait_time}s) al cargar la página.")
|
||||
|
||||
async def evaluar_js(self, js_code: str) -> Optional[str]:
|
||||
try:
|
||||
result = await self._enviar("Runtime.evaluate", {
|
||||
"expression": js_code,
|
||||
"returnByValue": True
|
||||
})
|
||||
if "exceptionDetails" in result:
|
||||
raise Exception(result["exceptionDetails"])
|
||||
return result.get("result", {}).get("value")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al ejecutar JS: {e}")
|
||||
return None
|
||||
|
||||
async def inyectar_archivo_js(self, ruta_archivo: str, reemplazos: dict = None) -> Optional[str]:
|
||||
if not os.path.exists(ruta_archivo):
|
||||
print(f"❌ Archivo JS no encontrado: {ruta_archivo}")
|
||||
return None
|
||||
|
||||
with open(ruta_archivo, "r", encoding="utf-8") as f:
|
||||
js_code = f.read()
|
||||
|
||||
if reemplazos:
|
||||
for key, value in reemplazos.items():
|
||||
js_code = js_code.replace(f"{{{{{key}}}}}", str(value))
|
||||
|
||||
# 🔧 Eliminamos el `return` externo
|
||||
js_code_final = f"(async () => {{\n{js_code}\n}})();"
|
||||
|
||||
try:
|
||||
result = await self._enviar("Runtime.evaluate", {
|
||||
"expression": js_code_final,
|
||||
"returnByValue": True
|
||||
})
|
||||
if "exceptionDetails" in result:
|
||||
raise Exception(result["exceptionDetails"])
|
||||
return result.get("result", {}).get("value")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al inyectar JS desde {ruta_archivo}: {e}")
|
||||
return None
|
||||
|
||||
async def obtener_user_agent(self) -> Optional[str]:
|
||||
return await self.evaluar_js("navigator.userAgent")
|
||||
|
||||
async def capturar_screenshot(self, output_path: str = "screenshot.png"):
|
||||
try:
|
||||
result = await self._enviar("Page.captureScreenshot")
|
||||
data = result["data"]
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(base64.b64decode(data))
|
||||
if self.verbose:
|
||||
print(f"📸 Screenshot guardado como {output_path}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al capturar screenshot: {e}")
|
||||
|
||||
async def cerrar(self):
|
||||
try:
|
||||
if not self.websocket.closed:
|
||||
await self.websocket.close()
|
||||
if self.verbose:
|
||||
print("🛑 WebSocket cerrado.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al cerrar pestaña: {e}")
|
||||
|
||||
async def obtener_html_completo(self) -> Optional[str]:
|
||||
try:
|
||||
result = await self._enviar("Runtime.evaluate", {
|
||||
"expression": "document.documentElement.outerHTML",
|
||||
"returnByValue": True
|
||||
})
|
||||
return result.get("result", {}).get("value")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al obtener HTML: {e}")
|
||||
return None
|
||||
|
||||
async def obtener_dominio(self) -> Optional[str]:
|
||||
try:
|
||||
dominio = await self.evaluar_js("window.location.hostname")
|
||||
if self.verbose and dominio:
|
||||
print(f"🌐 Dominio actual: {dominio}")
|
||||
return dominio
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al obtener dominio: {e}")
|
||||
return None
|
||||
|
||||
async def get_element_by_selector_node(self, selector: str) -> Optional["ElementoWeb"]:
|
||||
try:
|
||||
doc = await self._enviar("DOM.getDocument")
|
||||
root_node_id = doc["root"]["nodeId"]
|
||||
|
||||
result = await self._enviar("DOM.querySelector", {
|
||||
"nodeId": root_node_id,
|
||||
"selector": selector
|
||||
})
|
||||
node_id = result.get("nodeId")
|
||||
|
||||
if not node_id:
|
||||
print(f"⚠️ Nodo no encontrado con selector: {selector}")
|
||||
return None
|
||||
|
||||
return ElementoWeb.from_node(self, node_id=node_id)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al buscar nodo desde DOM.querySelector: {e}")
|
||||
return None
|
||||
|
||||
async def get_elements_by_css_selector(self, selector: str) -> List["ElementoWeb"]:
|
||||
try:
|
||||
result = await self._enviar("Runtime.evaluate", {
|
||||
"expression": f'Array.from(document.querySelectorAll("{selector}"))',
|
||||
"objectGroup": "grupo_elementos_css",
|
||||
"includeCommandLineAPI": True,
|
||||
"returnByValue": False
|
||||
})
|
||||
array_id = result["result"]["objectId"]
|
||||
props = await self._enviar("Runtime.getProperties", {
|
||||
"objectId": array_id,
|
||||
"ownProperties": True
|
||||
})
|
||||
elementos = []
|
||||
for prop in props["result"]:
|
||||
if "value" in prop and "objectId" in prop["value"]:
|
||||
elementos.append(ElementoWeb(self, prop["value"]["objectId"]))
|
||||
if self.verbose:
|
||||
print(f"🔍 Se encontraron {len(elementos)} elementos con el selector CSS '{selector}'.")
|
||||
return elementos
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al buscar elementos por selector CSS '{selector}': {e}")
|
||||
return []
|
||||
|
||||
async def enfocar(self):
|
||||
try:
|
||||
await self._enviar("Page.bringToFront")
|
||||
if self.verbose:
|
||||
print("🪟 Pestaña enfocada (bringToFront).")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al enfocar pestaña: {e}")
|
||||
Reference in New Issue
Block a user