From cd15c68b4e126d91d6e4438c6ea87021738b96be Mon Sep 17 00:00:00 2001 From: Luka Date: Fri, 27 Feb 2026 12:21:17 +0100 Subject: [PATCH] Proyecto acabado --- __pycache__/config.cpython-312.pyc | Bin 2587 -> 2936 bytes __pycache__/system_utils.cpython-312.pyc | Bin 49744 -> 66811 bytes __pycache__/ui_layout.cpython-312.pyc | Bin 35622 -> 45295 bytes config.py | 49 +++-- data/local_chat.txt | 0 system_utils.py | 252 ++++++++++++++++++++++- ui_layout.py | 192 +++++++++++++++++ 7 files changed, 467 insertions(+), 26 deletions(-) create mode 100644 data/local_chat.txt diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc index 9ce5e533c55a9b59c38d01ae31674dc10e8e2c10..7deaaae65e739e5d2238ba1e9719ac502db6c41e 100644 GIT binary patch delta 1374 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 DuAmN} delta 6802 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 W()H6y-`Z5qPpboKJ)ScjwEqu?z^V=a diff --git a/__pycache__/ui_layout.cpython-312.pyc b/__pycache__/ui_layout.cpython-312.pyc index 8463c509face41be3483fb3b1b2862ddcf90d177..599a5cdd5aebd11442ccbd9828971b69b7e127c9 100644 GIT binary patch delta 14430 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 delta 7874 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 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...")