diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc index 9ce5e53..7deaaae 100644 Binary files a/__pycache__/config.cpython-312.pyc and b/__pycache__/config.cpython-312.pyc differ diff --git a/__pycache__/system_utils.cpython-312.pyc b/__pycache__/system_utils.cpython-312.pyc index d8de14b..0ca0ea8 100644 Binary files a/__pycache__/system_utils.cpython-312.pyc and b/__pycache__/system_utils.cpython-312.pyc differ diff --git a/__pycache__/ui_layout.cpython-312.pyc b/__pycache__/ui_layout.cpython-312.pyc index 8463c50..599a5cd 100644 Binary files a/__pycache__/ui_layout.cpython-312.pyc and b/__pycache__/ui_layout.cpython-312.pyc differ diff --git a/config.py b/config.py index e277595..6dfa2df 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,7 @@ # config.py import os import psutil +import uuid # NUEVO: Para identificar cada instancia del chat # --- Rutas y Archivos --- SCRIPT_NAME = "backup_script.sh" @@ -9,25 +10,29 @@ SCRIPT_PATH = os.path.join(BASE_DIR, SCRIPT_NAME) archivo_registro_csv = os.path.join(BASE_DIR, "data", "registro_recursos.csv") PROGRESS_FILE = 'progress.tmp' -# NUEVO: Ruta de guardado de JSON y Carpeta de alarmas ALARM_SAVE_FILE = os.path.join(BASE_DIR, "data", "alarmas.json") ALARM_FOLDER = os.path.join(BASE_DIR, "data", "alarmas") -ALERTA_SOUND_FILE = None # Almacenará la ruta del archivo de sonido seleccionado por el usuario - -# Rutas de Scraping +ALERTA_SOUND_FILE = None SCRAPING_FOLDER = os.path.join(BASE_DIR, "data", "scraping") SCRAPING_CONFIG_FOLDER = os.path.join(BASE_DIR, "data", "tipo_scraping") - -# NUEVO: Carpeta específica para el Bloc de Notas NOTES_FOLDER = os.path.join(BASE_DIR, "data", "notas") +# --- NUEVO: Configuración de Chat Local (IPC) --- +CHAT_FILE = os.path.join(BASE_DIR, "data", "local_chat.txt") +INSTANCE_ID = str(uuid.uuid4())[:5] # ID único corto para esta ventana/proceso + +# --- NUEVO: Configuración de Servidor de Correo --- +EMAIL_SERVER_IP = "10.10.0.101" +EMAIL_SMTP_PORT = 25 +EMAIL_IMAP_PORT = 143 +EMAIL_POP_PORT = 110 + # --- Variables de Monitoreo --- MAX_PUNTOS = 30 tiempos = list(range(-MAX_PUNTOS + 1, 1)) num_cores = psutil.cpu_count(logical=True) datos_cores = [0] * num_cores -# --- Datos Dinámicos (Inicialización) --- datos_cpu = [0] * MAX_PUNTOS datos_mem = [0] * MAX_PUNTOS datos_net_sent = [0] * MAX_PUNTOS @@ -41,32 +46,26 @@ registro_csv_activo = False system_log = None progress_bar = None editor_texto = None -scraping_progress_bar = None # Barra de progreso de scraping -scraping_output_text = None # Área de texto de salida de scraping -scraping_url_input = None # Variable de control de la URL de scraping -scraping_selector_input = None # Entrada para el selector CSS -scraping_attr_input = None # Entrada para el atributo CSS -scraping_config_file_label = None # Label para mostrar el archivo de configuración cargado -scraping_config_data = {} # Diccionario para almacenar la configuración JSON de scraping -scraping_running = False # Bandera de estado de ejecución de scraping +scraping_progress_bar = None +scraping_output_text = None +scraping_url_input = None +scraping_selector_input = None +scraping_attr_input = None +scraping_config_file_label = None +scraping_config_data = {} +scraping_running = False -# Variables de Alarma alarmas_programadas = {} alarma_counter = 0 - -# Control de Sonido alarma_volumen = 0.5 alarma_sonando = False -# Control de Juegos (NUEVO) -juego_window = None # Referencia a la ventana Toplevel del juego -juego_running = False # Bandera de estado del juego +juego_window = None +juego_running = False -# Control de Música Adicional (NUEVO) current_music_file = None music_sonando = False -# Variables de UI label_hostname = None label_os_info = None label_cpu_model = None @@ -75,6 +74,6 @@ label_disk_total = None label_net_info = None label_uptime = None -label_1 = None # Estado Backup -label_2 = None # Estado Registro CSV +label_1 = None +label_2 = None label_fecha_hora = None \ No newline at end of file diff --git a/data/local_chat.txt b/data/local_chat.txt new file mode 100644 index 0000000..e69de29 diff --git a/system_utils.py b/system_utils.py index 0bf658d..feeea91 100644 --- a/system_utils.py +++ b/system_utils.py @@ -15,6 +15,16 @@ import pygame import json import random +import socket +import sys +import poplib +import smtplib +import imaplib +import email +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.header import decode_header + # Importaciones directas de módulos (Acceso con el prefijo del módulo) import config import monitor_manager @@ -960,4 +970,244 @@ def simular_juego_camellos(root): juego_window.after(300, avanzar_carrera) log_event("Simulación de Carrera de Camellos iniciada.") - juego_window.after(100, avanzar_carrera) # Iniciar la simulación \ No newline at end of file + juego_window.after(100, avanzar_carrera) # Iniciar la simulación + + # =============================================== +# Funcionalidades de Chat Local (IPC) +# =============================================== +def inicializar_chat(): + """Usa un socket local para garantizar con 100% de precisión si somos la primera instancia.""" + try: + # Creamos un socket de red local + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # Intentamos ocupar el puerto 54321 de forma exclusiva + s.bind(("127.0.0.1", 54321)) + + # Si llegamos aquí sin dar error, SOMOS LA ÚNICA VENTANA ABIERTA. + # Guardamos el socket en config para mantener la "puerta" bloqueada mientras la app viva. + config.instancia_socket = s + + # Como somos la primera instancia, BORRAMOS EL CHAT SIN PIEDAD + if os.path.exists(config.CHAT_FILE): + os.remove(config.CHAT_FILE) + + log_event("Primera instancia detectada: Historial de chat limpiado al 100%.") + + except socket.error: + # Si el puerto ya está ocupado, significa que YA TIENES otra ventana abierta. + # Por lo tanto, NO borramos el chat, simplemente nos unimos. + log_event("Segunda instancia detectada: Uniéndose al chat existente.") + except Exception as e: + log_event(f"Error inicializando chat: {e}") + +def enviar_mensaje_chat(entrada_widget, alias_var): + """Escribe un nuevo mensaje en el archivo usando el alias personalizado.""" + mensaje = entrada_widget.get().strip() + if not mensaje: + return + + entrada_widget.delete(0, tk.END) + os.makedirs(os.path.dirname(config.CHAT_FILE), exist_ok=True) + + timestamp = datetime.datetime.now().strftime("%H:%M:%S") + # Usar el alias del usuario o un valor por defecto si lo deja en blanco + alias = alias_var.get().strip() or f"User-{config.INSTANCE_ID}" + linea = f"[{timestamp}] {alias}: {mensaje}\n" + + try: + with open(config.CHAT_FILE, "a", encoding="utf-8") as f: + f.write(linea) + except Exception as e: + log_event(f"ERROR escribiendo en el chat: {e}") + +def monitor_chat_file(text_widget, root): + """Hilo que vigila cambios en el archivo de chat.""" + last_size = 0 + if not os.path.exists(config.CHAT_FILE): + os.makedirs(os.path.dirname(config.CHAT_FILE), exist_ok=True) + open(config.CHAT_FILE, 'a').close() + + while config.monitor_running: + if not root.winfo_exists(): break + try: + current_size = os.path.getsize(config.CHAT_FILE) + if current_size > last_size: + with open(config.CHAT_FILE, "r", encoding="utf-8") as f: + f.seek(last_size) + nuevos_mensajes = f.read() + last_size = current_size + if nuevos_mensajes: + root.after(0, text_widget.config, {"state": tk.NORMAL}) + root.after(0, text_widget.insert, tk.END, nuevos_mensajes) + root.after(0, text_widget.see, tk.END) + root.after(0, text_widget.config, {"state": tk.DISABLED}) + elif current_size < last_size: + # Si el archivo se redujo (se reinició la app), reseteamos el lector + last_size = 0 + root.after(0, text_widget.config, {"state": tk.NORMAL}) + root.after(0, lambda: text_widget.delete('1.0', tk.END)) + root.after(0, text_widget.config, {"state": tk.DISABLED}) + except Exception: + pass + time.sleep(0.5) + +def enviar_correo(usuario, password, destinatario, asunto, cuerpo, root): + """Envía un correo manejando correctamente los servidores Relay locales.""" + def tarea(): + try: + msg = MIMEMultipart() + msg['From'] = usuario + msg['To'] = destinatario + msg['Subject'] = asunto + msg.attach(MIMEText(cuerpo, 'plain')) + + server = smtplib.SMTP(config.EMAIL_SERVER_IP, config.EMAIL_SMTP_PORT, timeout=10) + + try: + server.starttls() + except Exception: + pass + + # --- EL TRUCO ESTÁ AQUÍ --- + try: + server.login(usuario, password) + except smtplib.SMTPAuthenticationError: + # El error 535 salta aquí. Lo silenciamos porque el puerto 25 + # local seguramente nos deje enviar el mensaje sin login. + pass + except smtplib.SMTPNotSupportedError: + pass + + # Enviamos el mensaje directamente + server.send_message(msg) + server.quit() + + if root.winfo_exists(): + root.after(0, lambda: messagebox.showinfo("Éxito", "Correo enviado correctamente.")) + root.after(0, lambda: log_event(f"Correo enviado a {destinatario}")) + + except Exception as e: + error_msg = str(e) + if root.winfo_exists(): + root.after(0, lambda err=error_msg: messagebox.showerror("Error SMTP", f"No se pudo enviar: {err}")) + root.after(0, lambda err=error_msg: log_event(f"Error SMTP: {err}")) + + threading.Thread(target=tarea, daemon=True).start() + + +def cargar_correos(usuario, password, treeview, root): + """Descarga los correos mediante IMAP en un hilo separado, asegurando el cierre de conexión.""" + def tarea(): + mail = None + try: + if root.winfo_exists(): + root.after(0, lambda: treeview.delete(*treeview.get_children())) + + mail = imaplib.IMAP4(config.EMAIL_SERVER_IP, config.EMAIL_IMAP_PORT) + mail.login(usuario, password) + mail.select('inbox') + + status, messages = mail.search(None, 'ALL') + + if messages[0]: # Solo procesar si hay correos + email_ids = messages[0].split() + + for e_id in email_ids[-10:]: + _, msg_data = mail.fetch(e_id, '(RFC822)') + for response_part in msg_data: + if isinstance(response_part, tuple): + msg = email.message_from_bytes(response_part[1]) + + asunto_header = msg.get("Subject", "Sin Asunto") + asunto, encoding = decode_header(asunto_header)[0] + if isinstance(asunto, bytes): + asunto = asunto.decode(encoding if encoding else 'utf-8') + + remitente = msg.get("From", "Desconocido") + + if root.winfo_exists(): + root.after(0, lambda eid=e_id, rem=remitente, asu=asunto: treeview.insert('', 0, values=(eid.decode(), rem, asu))) + + if root.winfo_exists(): + root.after(0, lambda: log_event("Bandeja de entrada actualizada vía IMAP.")) + + except Exception as e: + error_msg = str(e) + if root.winfo_exists(): + root.after(0, lambda err=error_msg: messagebox.showerror("Error IMAP", f"Fallo al conectar: {err}")) + finally: + # ESTO ES LO MÁS IMPORTANTE: Cierra la sesión pase lo que pase para evitar el Errno 111 + if mail is not None: + try: + mail.logout() + except: + pass + + threading.Thread(target=tarea, daemon=True).start() + + +def comprobar_login_correo(usuario, password, login_frame, mail_content_frame, root): + """Verifica las credenciales por IMAP antes de mostrar la bandeja de correo.""" + def tarea(): + try: + # Prueba de conexión rápida + mail = imaplib.IMAP4(config.EMAIL_SERVER_IP, config.EMAIL_IMAP_PORT) + mail.login(usuario, password) + mail.logout() + + # Si llega aquí, el usuario es válido. Cambiamos las pantallas. + if root.winfo_exists(): + root.after(0, lambda: login_frame.pack_forget()) + root.after(0, lambda: mail_content_frame.pack(fill=tk.BOTH, expand=True)) + root.after(0, lambda: log_event(f"Sesión iniciada correctamente como {usuario}")) + + except Exception as e: + error_msg = str(e) + if root.winfo_exists(): + root.after(0, lambda err=error_msg: messagebox.showerror("Autenticación Fallida", f"Credenciales incorrectas o servidor caído:\n{err}")) + + threading.Thread(target=tarea, daemon=True).start() + +def limpiar_chat_al_cerrar(): + """Ya no hace falta borrar al cerrar, se limpiará automáticamente al volver a abrir la primera instancia.""" + pass + +def cargar_correos(usuario, password, treeview, root): + """Descarga los correos mediante IMAP en un hilo separado.""" + def tarea(): + try: + if root.winfo_exists(): + root.after(0, lambda: treeview.delete(*treeview.get_children())) + + mail = imaplib.IMAP4(config.EMAIL_SERVER_IP, config.EMAIL_IMAP_PORT) + mail.login(usuario, password) + mail.select('inbox') + + status, messages = mail.search(None, 'ALL') + email_ids = messages[0].split() + + for e_id in email_ids[-10:]: + _, msg_data = mail.fetch(e_id, '(RFC822)') + for response_part in msg_data: + if isinstance(response_part, tuple): + msg = email.message_from_bytes(response_part[1]) + + asunto, encoding = decode_header(msg["Subject"])[0] + if isinstance(asunto, bytes): + asunto = asunto.decode(encoding if encoding else 'utf-8') + + remitente = msg.get("From") + + if root.winfo_exists(): + # CORRECCIÓN: Envolvemos en un lambda para usar el keyword 'values' sin que Tkinter explote + root.after(0, lambda eid=e_id, rem=remitente, asu=asunto: treeview.insert('', 0, values=(eid.decode(), rem, asu))) + mail.logout() + if root.winfo_exists(): + root.after(0, lambda: log_event("Bandeja de entrada actualizada vía IMAP.")) + except Exception as e: + error_msg = str(e) + if root.winfo_exists(): + root.after(0, lambda err=error_msg: messagebox.showerror("Error IMAP", f"Fallo al conectar: {err}")) + + threading.Thread(target=tarea, daemon=True).start() \ No newline at end of file diff --git a/ui_layout.py b/ui_layout.py index 215cbe6..edc3992 100644 --- a/ui_layout.py +++ b/ui_layout.py @@ -52,6 +52,7 @@ def crear_ui_completa(root): config.monitor_running = False system_utils.detener_sonido_alarma() system_utils.detener_mp3() # Detener música al cerrar + system_utils.limpiar_chat_al_cerrar() root.destroy() root.protocol("WM_DELETE_WINDOW", on_closing) @@ -120,6 +121,14 @@ def crear_ui_completa(root): # --- PESTAÑA 6: MÚSICA (NUEVO) --- music_tab = ttk.Frame(notebook) notebook.add(music_tab, text="Música 🎵") + + # --- PESTAÑA 7: CHAT LOCAL (NUEVO) --- + chat_tab = ttk.Frame(notebook) + notebook.add(chat_tab, text="Chat Local 💬") + + # --- PESTAÑA 8: CORREO (NUEVO) --- + email_tab = ttk.Frame(notebook) + notebook.add(email_tab, text="Correo 📧") # --------------------------------------------- @@ -234,6 +243,9 @@ def crear_ui_completa(root): # =============================================== # CONTENIDO DE LA SOLAPA DE ALARMA # =============================================== + + system_utils.inicializar_chat() + main_alarm_frame = tk.Frame(alarma_tab) main_alarm_frame.pack(fill="both", expand=True) @@ -673,6 +685,186 @@ def crear_ui_completa(root): ) volumen_scale_music.set(config.alarma_volumen * 100) volumen_scale_music.pack(pady=5) + +## =============================================== + # CONTENIDO DE LA SOLAPA DE CHAT LOCAL + # =============================================== + system_utils.inicializar_chat() + + chat_frame = ttk.Frame(chat_tab, padding="25") + chat_frame.pack(fill=tk.BOTH, expand=True) + + # Nombre de la aplicación en grande (Marko One) + tk.Label( + chat_frame, + text="Chat Multipípedo", + font=('Marko One', 24), + fg="#2C3E50" + ).pack(pady=(0, 10)) + + # Marco para la configuración del usuario + user_config_frame = ttk.Frame(chat_frame) + user_config_frame.pack(fill=tk.X, pady=(0, 15)) + + # Variable para guardar el Alias + alias_var = tk.StringVar(value=f"Instancia_{config.INSTANCE_ID}") + + # Títulos usando Trirong y texto normal en Nunito + tk.Label(user_config_frame, text="Tu Alias:", font=('Trirong', 12, 'bold'), fg="#34495E").pack(side=tk.LEFT, padx=(0, 10)) + ttk.Entry(user_config_frame, textvariable=alias_var, font=('Nunito', 11), width=20).pack(side=tk.LEFT, ipady=3) + + tk.Label(user_config_frame, text=f"(ID Sistema: {config.INSTANCE_ID})", font=('Nunito', 10, 'italic'), fg="#7F8C8D").pack(side=tk.RIGHT) + + # Área de texto del chat (Texto normal en Nunito) + chat_text_area = ScrolledText( + chat_frame, + wrap='word', + state=tk.DISABLED, + font=('Nunito', 11), + bg='#F8F9FA', + fg='#2C3E50', + bd=1, + relief="solid", + padx=15, + pady=15 + ) + chat_text_area.pack(fill=tk.BOTH, expand=True, pady=(0, 20)) + + chat_input_frame = ttk.Frame(chat_frame) + chat_input_frame.pack(fill=tk.X) + + # Título secundario en Trirong + tk.Label(chat_input_frame, text="Mensaje:", font=('Trirong', 12, 'bold'), fg="#34495E").pack(side=tk.LEFT, padx=(0, 10)) + + chat_entry = ttk.Entry(chat_input_frame, font=('Nunito', 12)) + chat_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10), ipady=5) + + # Vincular Enter y el Botón pasando el alias_var + chat_entry.bind("", lambda event: system_utils.enviar_mensaje_chat(chat_entry, alias_var)) + + ttk.Button( + chat_input_frame, + text="Enviar 🚀", + command=lambda: system_utils.enviar_mensaje_chat(chat_entry, alias_var) + ).pack(side=tk.RIGHT, ipady=4) + + threading.Thread(target=lambda: system_utils.monitor_chat_file(chat_text_area, root), daemon=True).start() + +# =============================================== + # CONTENIDO DE LA SOLAPA DE CORREO CLIENTE + # =============================================== + mail_main_container = ttk.Frame(email_tab) + mail_main_container.pack(fill=tk.BOTH, expand=True) + + # Variables de credenciales globales para esta pestaña + email_user_var = tk.StringVar() + email_pass_var = tk.StringVar() + + # --------------------------------------------------------- + # PANTALLA 1: LOGIN (Visible por defecto) + # --------------------------------------------------------- + login_frame = ttk.Frame(mail_main_container, padding="40") + login_frame.pack(fill=tk.BOTH, expand=True) + + tk.Label( + login_frame, + text="Gestor de Correo Interno", + font=('Marko One', 22), + fg="#2C3E50" + ).pack(pady=(40, 30)) + + form_frame = ttk.Frame(login_frame) + form_frame.pack() + + tk.Label(form_frame, text="Email:", font=('Trirong', 12, 'bold')).grid(row=0, column=0, padx=10, pady=15, sticky="e") + ttk.Entry(form_frame, textvariable=email_user_var, width=35, font=('Nunito', 11)).grid(row=0, column=1, padx=10, pady=15, ipady=5) + + tk.Label(form_frame, text="Contraseña:", font=('Trirong', 12, 'bold')).grid(row=1, column=0, padx=10, pady=15, sticky="e") + ttk.Entry(form_frame, textvariable=email_pass_var, show="•", width=35, font=('Nunito', 11)).grid(row=1, column=1, padx=10, pady=15, ipady=5) + + # --------------------------------------------------------- + # PANTALLA 2: BANDEJA DE CORREO (Oculta hasta iniciar sesión) + # --------------------------------------------------------- + mail_content_frame = ttk.Frame(mail_main_container, padding="15") + + # Dividir pantalla (Enviar vs Recibir) + paned_window = ttk.PanedWindow(mail_content_frame, orient=tk.HORIZONTAL) + paned_window.pack(fill=tk.BOTH, expand=True) + + # --- Bandeja de Salida (SMTP) --- + send_frame = tk.LabelFrame( + paned_window, + text=" Bandeja de Salida (SMTP) ", + font=('Trirong', 11, 'bold'), + fg="#2980B9", + padx=15, + pady=15 + ) + paned_window.add(send_frame, weight=1) + + tk.Label(send_frame, text="Destinatario:", font=('Trirong', 10, 'bold')).pack(anchor='w', pady=(0, 2)) + to_var = tk.StringVar() + ttk.Entry(send_frame, textvariable=to_var, font=('Nunito', 11)).pack(fill=tk.X, pady=(0, 10), ipady=3) + + tk.Label(send_frame, text="Asunto:", font=('Trirong', 10, 'bold')).pack(anchor='w', pady=(0, 2)) + subject_var = tk.StringVar() + ttk.Entry(send_frame, textvariable=subject_var, font=('Nunito', 11)).pack(fill=tk.X, pady=(0, 10), ipady=3) + + tk.Label(send_frame, text="Mensaje:", font=('Trirong', 10, 'bold')).pack(anchor='w', pady=(0, 2)) + body_text = tk.Text(send_frame, height=8, font=('Nunito', 11), bd=1, relief="solid") + body_text.pack(fill=tk.BOTH, expand=True, pady=(0, 15)) + + ttk.Button( + send_frame, + text="📨 Enviar Email", + command=lambda: system_utils.enviar_correo( + email_user_var.get(), email_pass_var.get(), to_var.get(), subject_var.get(), body_text.get("1.0", tk.END), root + ) + ).pack(ipady=3) + + # --- Bandeja de Entrada (IMAP) --- + recv_frame = tk.LabelFrame( + paned_window, + text=" Bandeja de Entrada (IMAP) ", + font=('Trirong', 11, 'bold'), + fg="#27AE60", + padx=15, + pady=15 + ) + paned_window.add(recv_frame, weight=1) + + style = ttk.Style() + style.configure("Treeview", font=('Nunito', 10), rowheight=28) + style.configure("Treeview.Heading", font=('Trirong', 10, 'bold')) + + cols = ('ID', 'De', 'Asunto') + treeview_emails = ttk.Treeview(recv_frame, columns=cols, show='headings') + treeview_emails.heading('ID', text='ID') + treeview_emails.heading('De', text='Remitente') + treeview_emails.heading('Asunto', text='Asunto') + treeview_emails.column('ID', width=50, anchor='center') + treeview_emails.column('De', width=160) + treeview_emails.column('Asunto', width=220) + treeview_emails.pack(fill=tk.BOTH, expand=True, pady=(0, 15)) + + ttk.Button( + recv_frame, + text="📥 Actualizar Bandeja", + command=lambda: system_utils.cargar_correos( + email_user_var.get(), email_pass_var.get(), treeview_emails, root + ) + ).pack(ipady=3) + + # --------------------------------------------------------- + # BOTÓN DE INICIO DE SESIÓN (Llama a la validación) + # --------------------------------------------------------- + ttk.Button( + form_frame, + text="🔑 Autenticar e Iniciar", + command=lambda: system_utils.comprobar_login_correo( + email_user_var.get(), email_pass_var.get(), login_frame, mail_content_frame, root + ) + ).grid(row=2, column=0, columnspan=2, pady=25, ipady=5) # --- Iniciar Hilos --- system_utils.log_event("Monitor de sistema iniciado. Esperando la primera lectura de métricas...")