Compare commits

...

2 Commits

Author SHA1 Message Date
Luka df0f6059ee Merge branch 'main' of https://git.ieslamar.org/luka/ProyectoPHP 2026-02-27 12:22:15 +01:00
Luka cd15c68b4e Proyecto acabado 2026-02-27 12:21:17 +01:00
7 changed files with 466 additions and 22 deletions

Binary file not shown.

View File

@ -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"
@ -12,19 +13,26 @@ PROGRESS_FILE = 'progress.tmp'
ALARM_SAVE_FILE = os.path.join(BASE_DIR, "data", "alarmas.json")
ALARM_FOLDER = os.path.join(BASE_DIR, "data", "alarmas")
ALERTA_SOUND_FILE = None
SCRAPING_FOLDER = os.path.join(BASE_DIR, "data", "scraping")
SCRAPING_CONFIG_FOLDER = os.path.join(BASE_DIR, "data", "tipo_scraping")
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
@ -38,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
@ -72,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

0
data/local_chat.txt Normal file
View File

View File

@ -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
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()

View File

@ -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("<Return>", 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...")