#!/usr/bin/env python3 import os import subprocess import sys import threading import time import urllib.parse import json import requests # Estado por chat para la busqueda global interactiva (numero -> descarga) ultima_titulos = {} # chat_id -> {'query':..., 'results':[...]} ultimos_torrents = {} # chat_id -> {'titulo':..., 'torrents':[...]} flujo_genero = {} # chat_id -> 'esperando_tipo' | 'esperando_genero' | None flujo_genero_tipo = {} # chat_id -> 'movie' | 'series' (tipo ya elegido) panel_descargas = {} # chat_id -> estado del panel editable temporizadores_parental = {} # alias normalizado -> Timer de desbloqueo menu_pendiente = {} # chat_id -> accion de texto libre esperada (menu botones) menu_pausa_nombre = {} # chat_id -> nombre elegido en flujo de pausa historial_chat = {} # chat_id -> [message_id...] enviados por el bot (para /limpiar) borrar_pend = {} # chat_id -> lista de tareas NAS esperando numero a borrar BASE = os.path.dirname(os.path.abspath(__file__)) LOG = os.path.join(BASE, "bot.log") OFFSET_FILE = os.path.join(BASE, "bot.offset") TV_API = "http://127.0.0.1:8600/tv/mensaje" def log(linea): try: with open(LOG, "a") as f: f.write(f"{time.strftime('%H:%M:%S')} {linea}\n") except Exception: pass def load_env(): env = {} try: for line in open(os.path.join(BASE, ".env")): line = line.strip() if line and not line.startswith("#") and "=" in line: k, v = line.split("=", 1) env[k.strip()] = v.strip() except Exception: pass return env ENV = load_env() TOKEN = ENV.get("TELEGRAM_TOKEN", "") CHAT_ID = ENV.get("TELEGRAM_CHAT_ID", "") ALERTA_TEMP = float(ENV.get("ALERTA_TEMP", "80")) def sh(cmd): try: return subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10).stdout.strip() except Exception: return "n/a" def estado(): temp = sh("vcgencmd measure_temp") upt = sh("uptime -p") load = open("/proc/loadavg").read().split()[:3] free = sh("free -h | awk 'NR==2{print $2\" total, \"$7\" disponible\"}'") disk = sh("df -h / | awk 'NR==2{print $3\" usados de \"$2\" (\"$5\")\"}'") ips = sh("hostname -I") temp_c = None try: temp_c = float(temp.replace("'C", "").replace("temp=", "")) except Exception: pass msg = ( "📊 Estado de minipc:\n" f"🌡️ Temperatura: {temp}\n" f"🔌 Uptime: {upt}\n" f"⚙️ Carga: {', '.join(load)}\n" f"🧠 RAM: {free}\n" f"💾 Disco /: {disk}\n" f"🌐 IPs: {ips}" ) return msg, temp_c def enviar(texto, chat_id): # Trocea mensajes largos (Telegram limita a ~4096 chars) para no fallar en silencio ultimo_id = None for i in range(0, len(texto), 4000): bloque = texto[i:i + 4000] try: r = requests.post( f"https://api.telegram.org/bot{TOKEN}/sendMessage", data={"chat_id": chat_id, "text": bloque, "parse_mode": "HTML"}, timeout=10, ) d = r.json() if d.get("ok"): ultimo_id = d["result"].get("message_id") lista = historial_chat.setdefault(chat_id, []) lista.append(ultimo_id) if len(lista) > 300: historial_chat[chat_id] = lista[-300:] else: log(f"Telegram sendMessage error: {d.get('description', d)}") except Exception as e: log(f"Telegram sendMessage exception: {e}") return ultimo_id def teclado_descargas(): return { "inline_keyboard": [ [ {"text": "🔄 Actualizar", "callback_data": "dl:refresh"}, {"text": "▶️ Activas", "callback_data": "dl:active"}, ], [ {"text": "📁 Todas", "callback_data": "dl:all"}, {"text": "🗑️ Borrar…", "callback_data": "dl:del"}, {"text": "✖️ Cerrar", "callback_data": "dl:close"}, ], ] } def enviar_panel_descargas(texto, chat_id): try: r = requests.post( f"https://api.telegram.org/bot{TOKEN}/sendMessage", data={ "chat_id": chat_id, "text": texto, "parse_mode": "HTML", "reply_markup": json.dumps(teclado_descargas()), }, timeout=10, ) d = r.json() if d.get("ok"): return d["result"].get("message_id") log(f"Telegram panel send error: {d.get('description', d)}") except Exception as e: log(f"Telegram panel send exception: {e}") return None def editar_panel_descargas(chat_id, message_id, texto): try: r = requests.post( f"https://api.telegram.org/bot{TOKEN}/editMessageText", data={ "chat_id": chat_id, "message_id": message_id, "text": texto, "parse_mode": "HTML", "reply_markup": json.dumps(teclado_descargas()), }, timeout=10, ) d = r.json() if not d.get("ok") and "not modified" not in d.get("description", "").lower(): log(f"Telegram panel edit error: {d.get('description', d)}") return d.get("ok", False) except Exception as e: log(f"Telegram panel edit exception: {e}") return False def responder_callback(callback_id): try: requests.post( f"https://api.telegram.org/bot{TOKEN}/answerCallbackQuery", data={"callback_query_id": callback_id}, timeout=10, ) except Exception as e: log(f"Telegram callback error: {e}") def texto_panel_descargas(modo="active"): import nas_dl lineas, activas = nas_dl.listar_descargas( hablar_solo_activas=(modo == "active") ) encabezado = "📥 DESCARGAS DEL NAS" if modo == "active": encabezado += " · ACTIVAS" texto = encabezado + "\n\n" + "\n".join(lineas) texto += f"\n\n🕒 Actualizado: {time.strftime('%H:%M:%S')}" return texto, activas def abrir_panel_descargas(chat_id): anterior = panel_descargas.pop(chat_id, None) if anterior: anterior["stop"].set() try: texto, _activas = texto_panel_descargas() message_id = enviar_panel_descargas(texto, chat_id) if not message_id: enviar("❌ No se pudo abrir el panel de descargas.", chat_id) return estado_panel = { "message_id": message_id, "modo": "active", "stop": threading.Event(), } panel_descargas[chat_id] = estado_panel threading.Thread( target=monitor_panel_descargas, args=(chat_id, estado_panel), daemon=True, ).start() except Exception as e: log(f"panel descargas error: {e}") enviar(f"❌ Error en el panel de descargas: {e}", chat_id) def monitor_panel_descargas(chat_id, estado_panel): while not estado_panel["stop"].wait(20): try: texto, activas = texto_panel_descargas(estado_panel["modo"]) editar_panel_descargas(chat_id, estado_panel["message_id"], texto) if not activas: break except Exception as e: log(f"monitor panel descargas error: {e}") def procesar_callback(callback): callback_id = callback.get("id") mensaje = callback.get("message") or {} chat_id = (mensaje.get("chat") or {}).get("id") data = callback.get("data") or "" responder_callback(callback_id) # Menu de botones (menu_idd) if data.startswith("m:"): try: from menu_idd import procesar_callback as menu_cb menu_cb(data[2:], chat_id, mensaje.get("message_id")) except Exception as e: log(f"menu callback error: {e}") return accion = data.split(":", 1)[-1] estado_panel = panel_descargas.get(chat_id) if accion == "del": pedir_borrar(chat_id) return if not estado_panel: return if accion == "close": estado_panel["stop"].set() panel_descargas.pop(chat_id, None) borrar(chat_id, estado_panel["message_id"]) return if accion in ("active", "all"): estado_panel["modo"] = "active" if accion == "active" else "all" try: texto, _activas = texto_panel_descargas(estado_panel["modo"]) editar_panel_descargas(chat_id, estado_panel["message_id"], texto) except Exception as e: log(f"callback panel descargas error: {e}") def borrar(chat_id, message_id): try: requests.post( f"https://api.telegram.org/bot{TOKEN}/deleteMessage", data={"chat_id": chat_id, "message_id": message_id}, timeout=10, ) except Exception as e: print("Error borrando:", e) def limpiar_pantalla(chat_id): """Borra los mensajes que el bot ha enviado en este chat (/limpiar).""" lista = historial_chat.pop(chat_id, None) or [] borrados = 0 for mid in reversed(lista): try: r = requests.post( f"https://api.telegram.org/bot{TOKEN}/deleteMessage", data={"chat_id": chat_id, "message_id": mid}, timeout=8, ) if r.json().get("ok"): borrados += 1 except Exception: pass aviso = enviar(f"🧹 Pantalla limpia ({borrados} mensajes borrados).", chat_id) if aviso: threading.Timer(6.0, borrar, args=(chat_id, aviso)).start() def pedir_borrar(chat_id): """Pide que se elija el numero de la tarea NAS a borrar.""" try: import nas_dl except Exception as e: log(f"pedir_borrar import error: {e}") enviar(f"❌ Error cargando el módulo del NAS: {e}", chat_id) return try: tareas = nas_dl.listar_tareas() except Exception as e: log(f"listar tareas error: {e}") enviar(f"❌ Error listando descargas: {e}", chat_id) return if not tareas: enviar("No hay tareas en Download Station.", chat_id) return borrar_pend[chat_id] = tareas iconos = {"downloading": "▶️", "waiting": "⏳", "error": "❌", "finished": "✅", "paused": "⏸️", "seeding": "🌱"} lineas = ["🗑️ Responde el número de la tarea a BORRAR " "(o escribe cancelar):", ""] for i, t in enumerate(tareas, start=1): ic = iconos.get(t["status"], "•") titulo = (t["title"] or "?").strip() lineas.append(f"{i}. {ic} [{t['status']}] ({t['pct']:.0f}%) {titulo[:70]}") enviar("\n".join(lineas), chat_id) def _ejecutar_borrado(chat_id, numero): tareas = borrar_pend.pop(chat_id, None) if not tareas: return False if numero < 1 or numero > len(tareas): enviar("❌ Número no válido.", chat_id) return True tarea = tareas[numero - 1] try: import nas_dl except Exception as e: enviar(f"❌ Error cargando el módulo del NAS: {e}", chat_id) return True ok, detalle = nas_dl.borrar_tarea(tarea["id"]) titulo = (tarea["title"] or "?").strip() if ok: enviar(f"🗑️ Tarea borrada: {titulo[:80]}", chat_id) else: enviar(f"❌ No se pudo borrar {titulo[:60]}: {detalle}", chat_id) return True def mandar_tv(mensaje, simple): try: url = f"{TV_API}?texto={urllib.parse.quote(mensaje)}" if simple: url += "&simple=1" r = requests.get(url, timeout=8) return r.status_code == 200 except Exception as e: log(f"tv error: {e}") return False def mandar_youtube(url): try: r = requests.get(f"http://127.0.0.1:8600/tv/youtube?url={url}", timeout=10) return r.status_code == 200 except Exception as e: log(f"youtube error: {e}") return False def procesar(texto, chat_id): t = texto.lower().strip() log(f"RECIBIDO chat={chat_id} texto={texto!r}") partes = t.split(None, 1) comando = partes[0].split("@", 1)[0].lstrip("/") if partes else "" argumento = partes[1].strip() if len(partes) > 1 else "" # Texto libre pendiente pedido desde el menu de botones try: import menu_idd except Exception: menu_idd = None if menu_idd is not None: if t.startswith("/") or t in ("cancelar", "cancel", "salir"): if menu_idd.pendiente_activo(chat_id): menu_idd.cancelar_pendiente(chat_id) if t in ("cancelar", "cancel", "salir"): return elif menu_idd.pendiente_activo(chat_id): if menu_idd.resolver_pausa_minutos(chat_id, texto): return cmd = menu_idd.construir_comando(chat_id, texto) menu_idd.cancelar_pendiente(chat_id) if cmd == "INVALIDO": enviar("⏱️ Escribe un número de minutos entre 1 y 1440.", chat_id) return if cmd: return procesar(cmd, chat_id) # Borrado de descargas pendiente (numero de tarea NAS) if t.isdigit() and chat_id in borrar_pend: _ejecutar_borrado(chat_id, int(t)) return # Flujo interactivo por genero (espera numeros en pasos) if flujo_genero.get(chat_id) and t.isdigit(): paso = flujo_genero[chat_id] if paso == "esperando_tipo": if t == "1": flujo_genero_tipo[chat_id] = "movie" flujo_genero[chat_id] = "esperando_genero" _pedir_genero(chat_id, "🎬") elif t == "2": flujo_genero_tipo[chat_id] = "series" flujo_genero[chat_id] = "esperando_genero" _pedir_genero(chat_id, "📺") else: enviar("Opción inválida. Responde 1 para Películas o 2 para Series.", chat_id) elif paso == "esperando_genero": n = int(t) gol = _import_global_buscar() if 1 <= n <= len(gol.GENEROS): genero = gol.GENEROS[n - 1] tipo = flujo_genero_tipo.get(chat_id, "movie") flujo_genero.pop(chat_id, None) flujo_genero_tipo.pop(chat_id, None) _mostrar_catalogo_genero(chat_id, tipo, genero, "top") else: enviar("Número de género inválido.", chat_id) return if t in ("genero", "/genero"): procesar_genero(chat_id) elif t in ("estado", "/estado", "status", "/status"): msg, _ = estado() enviar(msg, chat_id) elif "temperatura" in t or "temp" in t: temp = sh("vcgencmd measure_temp") enviar(f"🌡️ Temperatura de la Pi: {temp}", chat_id) elif t.startswith("tv ") or t.startswith("/tv "): msj = texto.split(" ", 1)[1].strip() if not msj: enviar("Uso: tv <mensaje> (mensaje rapido a la tele)", chat_id) else: ok = mandar_tv(msj, simple=True) enviar("📺 Mensaje enviado a la tele ✅" if ok else "❌ No se pudo enviar a la tele", chat_id) elif t.startswith("alarma ") or t.startswith("/alarma "): msj = texto.split(" ", 1)[1].strip() if not msj: enviar("Uso: alarma <mensaje> (secuencia completa: sirena x3)", chat_id) else: ok = mandar_tv(msj, simple=False) enviar("🚨 Alarma lanzada en la tele ✅" if ok else "❌ No se pudo lanzar la alarma", chat_id) elif t.startswith("panorama") or t.startswith("/panorama"): enviar("📷 Capturando snapshots de las cámaras...", chat_id) try: import subprocess as sp r = sp.run(["python3", "/home/pi/minipc/vigilancia/panorama.py"], capture_output=True, text=True, timeout=60, cwd="/home/pi/minipc/vigilancia") log(f"panorama: {r.stdout.strip()[-200:]}") except Exception as e: log(f"panorama error: {e}") elif t in ("limpieza", "/limpieza", "limpiar", "/limpiar", "clear", "/clear"): if t in ("limpieza", "/limpieza"): enviar("🧹 Ejecutando limpieza de procesos...", chat_id) try: from limpiar import limpiar resultado = limpiar() enviar(f"
{resultado}
", chat_id) except Exception as e: log(f"limpieza error: {e}") enviar(f"❌ Error: {e}", chat_id) else: limpiar_pantalla(chat_id) elif t.startswith("youtube ") or t.startswith("/youtube "): url = texto.split(" ", 1)[1].strip() if not url: enviar("Uso: youtube <URL>", chat_id) else: ok = mandar_youtube(url) enviar("🎬 YouTube lanzado en la TV ✅" if ok else "❌ No se pudo lanzar YouTube", chat_id) elif comando == "borrar" and not argumento: pedir_borrar(chat_id) elif comando == "descargas" and argumento.lower() == "borrar": pedir_borrar(chat_id) elif t in ("descargas", "/descargas") or (comando == "descargar" and not argumento): abrir_panel_descargas(chat_id) elif comando == "descargar": try: import nas_dl if argumento: ok = nas_dl.add_torrent(argumento) enviar("📥 Añadido a Download Station del NAS ✅" if ok else "❌ No se pudo añadir al NAS. Revisa bot.log.", chat_id) except Exception as e: log(f"descargar error: {e}") enviar(f"❌ Error en Download Station: {e}", chat_id) elif comando == "termo": accion = argumento.lower().strip() acciones = {"on": "on", "encender": "on", "off": "off", "apagar": "off", "estado": "status", "status": "status"} if accion not in acciones: enviar("Uso: /termo on, /termo off o /termo estado", chat_id) else: try: from tapo_control import controlar_termo resultado = controlar_termo(acciones[accion]) enviar(f"🔌 {resultado}", chat_id) except Exception as e: log(f"termo error: {e}") enviar(f"❌ Error controlando el termo: {e}", chat_id) elif comando == "router": partes_router = argumento.lower().split() accion_router = partes_router[0] if partes_router else "ayuda" if accion_router in ("ayuda", "help"): enviar( "ROUTER ASUS\n" "/router estado — modelo, firmware, IP y uptime\n" "/router clientes — tabla ARP de clientes\n" "/router wifi — radios Wi-Fi\n" "/router inventario — leases DHCP con IP y MAC\n" "/router parental — configuración parental actual\n" "/router reservar confirmar — fija las IP actuales\n" "/router reiniciar confirmar — reinicio protegido", chat_id, ) elif accion_router in ("estado", "status"): try: from router_control import estado as router_estado enviar("📡 ASUS\n" + router_estado(), chat_id) except Exception as e: log(f"router estado error: {e}") enviar(f"❌ Error consultando ASUS: {e}", chat_id) elif accion_router in ("clientes", "clients"): try: from router_control import clientes enviar("📶 CLIENTES ASUS\n" + clientes(), chat_id) except Exception as e: log(f"router clientes error: {e}") enviar(f"❌ Error consultando clientes: {e}", chat_id) elif accion_router == "wifi": try: from router_control import wifi enviar("📡 WIFI ASUS\n" + wifi(), chat_id) except Exception as e: log(f"router wifi error: {e}") enviar(f"❌ Error consultando Wi-Fi: {e}", chat_id) elif accion_router == "inventario": try: from router_control import inventario enviar("📋 INVENTARIO DHCP ASUS\n" + inventario(), chat_id) except Exception as e: log(f"router inventario error: {e}") enviar(f"❌ Error consultando inventario: {e}", chat_id) elif accion_router in ("parental", "control-parental"): try: from router_control import control_parental enviar("👪 CONTROL PARENTAL ASUS\n" + control_parental(), chat_id) except Exception as e: log(f"router parental error: {e}") enviar(f"❌ Error consultando control parental: {e}", chat_id) elif accion_router == "reservar": if len(partes_router) < 2 or partes_router[1] != "confirmar": enviar("Para reservar las IP actuales responde exactamente: /router reservar confirmar", chat_id) else: try: from router_control import reservar_dhcp enviar("⏳ Aplicando reservas DHCP...", chat_id) enviar(f"✅ {reservar_dhcp()}", chat_id) except Exception as e: log(f"router reservar error: {e}") enviar(f"❌ Error aplicando reservas: {e}", chat_id) elif accion_router in ("reiniciar", "reboot"): if len(partes_router) < 2 or partes_router[1] != "confirmar": enviar("Para reiniciar responde exactamente: /router reiniciar confirmar", chat_id) else: try: from router_control import reiniciar enviar("⚠️ Reiniciando el ASUS...", chat_id) reiniciar() except Exception as e: log(f"router reinicio error: {e}") enviar(f"❌ Error reiniciando ASUS: {e}", chat_id) else: enviar("Acción de router no reconocida. Usa /router ayuda.", chat_id) elif comando == "parental": partes_parental = argumento.split() accion_parental = partes_parental[0].lower() if partes_parental else "listar" try: from asus_api_control import cambiar_bloqueo, normalizar_nombre if accion_parental == "listar": from router_control import control_parental enviar("👪 CONTROL PARENTAL ASUS\n" + control_parental(), chat_id) elif accion_parental in ("desactivar", "bloquear") and len(partes_parental) >= 2: alias = " ".join(partes_parental[1:]) enviar(f"🔒 {cambiar_bloqueo(alias, True)}", chat_id) elif accion_parental in ("activar", "desbloquear") and len(partes_parental) >= 2: alias = " ".join(partes_parental[1:]) timer = temporizadores_parental.pop(normalizar_nombre(alias), None) if timer: timer.cancel() enviar(f"🔓 {cambiar_bloqueo(alias, False)}", chat_id) elif accion_parental == "pausa" and len(partes_parental) >= 3: alias = " ".join(partes_parental[1:-1]) minutos = int(partes_parental[-1]) if minutos < 1 or minutos > 1440: raise ValueError("Los minutos deben estar entre 1 y 1440") cambiar_bloqueo(alias, True) clave = normalizar_nombre(alias) anterior = temporizadores_parental.pop(clave, None) if anterior: anterior.cancel() timer = threading.Timer( minutos * 60, lambda: cambiar_bloqueo(alias, False), ) timer.daemon = True temporizadores_parental[clave] = timer timer.start() enviar(f"⏸️ Bloqueado {alias} durante {minutos} minutos.", chat_id) else: enviar( "Uso: /parental pausa Inés 60, " "/parental desactivar Carolina o " "/parental activar Inés.", chat_id) except Exception as e: log(f"parental error: {e}") enviar(f"❌ Error en control parental: {e}", chat_id) elif comando in ("aimesh", "aiMesh", "mesh"): partes_mesh = argumento.split() accion_mesh = partes_mesh[0].lower() if partes_mesh else "estado" try: from asus_api_control import ( aimesh_estado, aimesh_reconstruir, aimesh_reiniciar_nodo, ) if accion_mesh in ("estado", "nodos", "listar"): enviar("🌐 AIMESH ASUS\n" + aimesh_estado(), chat_id) elif accion_mesh == "reiniciar" and len(partes_mesh) >= 2: if partes_mesh[-1] != "confirmar": enviar("Para reiniciar un nodo responde: /aiMesh reiniciar <nodo> confirmar", chat_id) else: nodo = " ".join(partes_mesh[1:-1]) enviar(f"🔄 Reiniciando el nodo {nodo}...", chat_id) enviar(aimesh_reiniciar_nodo(nodo), chat_id) elif accion_mesh == "reconstruir": if len(partes_mesh) < 2 or partes_mesh[-1] != "confirmar": enviar("Para reconstruir la malla responde: /aiMesh reconstruir confirmar", chat_id) else: enviar("⚠️ Reconstruyendo la malla AiMesh...", chat_id) enviar(aimesh_reconstruir(), chat_id) else: enviar( "Uso:\n/aiMesh estado — nodos y señal\n" "/aiMesh reiniciar <nodo> confirmar\n" "/aiMesh reconstruir confirmar", chat_id) except Exception as e: log(f"aiMesh error: {e}") enviar(f"❌ Error en AiMesh: {e}", chat_id) elif comando == "qos": partes_qos = argumento.lower().split() accion_qos = partes_qos[0] if partes_qos else "estado" try: from router_control import qos_apagar, qos_estado, qos_set if accion_qos == "estado": enviar("⚙️ QOS ASUS\n" + qos_estado(), chat_id) elif accion_qos in ("adaptive", "tradicional") and len(partes_qos) >= 2 and partes_qos[-1] == "confirmar": enviar("⏳ Aplicando QoS...", chat_id) enviar(qos_set(accion_qos), chat_id) elif accion_qos == "apagar" and len(partes_qos) >= 2 and partes_qos[-1] == "confirmar": enviar("⏳ Apagando QoS...", chat_id) enviar(qos_apagar(), chat_id) else: enviar( "Uso:\n/qos estado\n" "/qos adaptive confirmar — auto gaming/streaming\n" "/qos tradicional confirmar — prioridades manuales\n" "/qos apagar confirmar", chat_id) except Exception as e: log(f"qos error: {e}") enviar(f"❌ Error en QoS: {e}", chat_id) elif comando == "limite": partes_lim = argumento.split() accion_lim = partes_lim[0].lower() if partes_lim else "listar" try: from router_control import limite_listar, limite_poner, limite_quitar if accion_lim == "listar": enviar("🚦 LÍMITES DE VELOCIDAD\n" + limite_listar(), chat_id) elif accion_lim in ("poner", "set") and len(partes_lim) >= 4: try: dl = float(partes_lim[-2]) ul = float(partes_lim[-1]) except ValueError: enviar("Las velocidades deben ser números en Mbps.", chat_id) else: dispositivo = " ".join(partes_lim[1:-2]) or partes_lim[1] enviar(limite_poner(dispositivo, dl, ul), chat_id) elif accion_lim in ("quitar", "del", "remove") and len(partes_lim) >= 2: dispositivo = " ".join(partes_lim[1:]) enviar(limite_quitar(dispositivo), chat_id) else: enviar( "Uso:\n/limite listar\n" "/limite poner <dispositivo> <bajada> <subida> (Mbps)\n" " ✏️ Ej: /limite poner NAS 50 20\n" "/limite quitar <dispositivo>", chat_id) except Exception as e: log(f"limite error: {e}") enviar(f"❌ Error en límites: {e}", chat_id) elif comando == "seguridad": partes_seg = argumento.lower().split() accion_seg = partes_seg[0] if partes_seg else "estado" try: from router_control import ( seguridad_estado, seguridad_keyword, ) if accion_seg == "estado": enviar("🛡️ SEGURIDAD ASUS\n" + seguridad_estado(), chat_id) elif accion_seg in ("bloquear", "desbloquear") and len(partes_seg) >= 2: palabra = " ".join(partes_seg[1:]) activar = accion_seg == "bloquear" enviar(seguridad_keyword(palabra, activar), chat_id) else: enviar( "Uso (filtro de contenidos AiProtection):\n" "/seguridad estado\n" "/seguridad bloquear <palabra> — p. ej. tiktok\n" "/seguridad desbloquear <palabra>", chat_id) except Exception as e: log(f"seguridad error: {e}") enviar(f"❌ Error en seguridad: {e}", chat_id) elif t in ("pelis", "/pelis"): procesar_pelis(chat_id) elif t in ("series", "/series"): procesar_series(chat_id) elif t.startswith("pelicula ") or t.startswith("/pelicula "): nombre = texto.split(" ", 1)[1].strip() procesar_buscador_pelicula(nombre, chat_id) elif t.startswith("serie ") or t.startswith("/serie "): nombre = texto.split(" ", 1)[1].strip() procesar_buscador_serie(nombre, chat_id) elif t in ("novedades", "/novedades"): procesar_novedades(chat_id) elif t.startswith("busca ") or t.startswith("/busca "): nombre = texto.split(" ", 1)[1].strip() procesar_busca(nombre, chat_id) elif t.startswith("torrents ") or t.startswith("/torrents "): n = _int_arg(texto) procesar_torrents(n, chat_id) elif t.startswith("bajar ") or t.startswith("/bajar "): n = _int_arg(texto) procesar_bajar(n, chat_id) elif comando in ("menu", "menú", "inicio", "start") and not argumento: try: from menu_idd import enviar_menu enviar_menu(chat_id) except Exception as e: log(f"menu error: {e}") enviar(f"❌ Error abriendo el menú: {e}", chat_id) elif t in ("ayuda", "/ayuda", "help", "/help"): enviar( "📚 ÍNDICE DE COMANDOS\n" "🧭 Escribe /menu para navegar con botones.\n" "Escribe /ayuda cuando lo necesites.\n\n" "━━━ 🎬 VIDEOCLUB ━━━\n" "1. busca <nombre> — buscar en TODO internet\n" " ✏️ Ej: /busca duna\n" "2. genero — catálogo por género (30 más descargadas)\n" " ✏️ Ej: /genero → 1 (pelis) → 12 (Horror)\n" "3. torrents <n> — torrents del resultado n\n" " ✏️ Ej: /torrents 1\n" "4. bajar <n> — descargar al NAS el torrent n\n" " ✏️ Ej: /bajar 1\n" "5. pelicula <nombre> — buscar película (catálogo)\n" " ✏️ Ej: /pelicula ant-man\n" "6. serie <nombre> — buscar serie (catálogo)\n" " ✏️ Ej: /serie breaking bad\n" "7. novedades — últimas novedades del catálogo\n" " ✏️ Ej: /novedades\n\n" "━━━ 🖥️ NAS / DESCARGAS ━━━\n" "8. descargas — panel con progreso y botones\n" " ▶️ En el panel: botón 🗑️ Borrar… para eliminar una tarea\n" " ✏️ Ej: /descargas o /borrar\n" "9. descargar <enlace> — añade a Download Station\n" " ✏️ Ej: /descargar magnet:?xt=urn:btih:...\n" "10. termo on|off|estado — controla el enchufe Tapo\n" " ✏️ Ej: /termo estado\n\n" "━━━ 📺 TV ━━━\n" "11. tv <mensaje> — mensaje rápido a la tele\n" " ✏️ Ej: /tv la cena está lista\n" "12. alarma <mensaje> — secuencia completa (sirena x3)\n" " ✏️ Ej: /alarma vienen a por vosotros\n" "13. youtube <URL> — reproduce YouTube en la TV\n" " ✏️ Ej: /youtube https://youtu.be/xxxx\n\n" "━━━ 👪 CONTROL PARENTAL ━━━\n" "14. parental listar — estado y dispositivos\n" " ✏️ Ej: /parental listar\n" "15. parental desactivar|activar — bloquea o desbloquea\n" " ✏️ Ej: /parental desactivar Inés\n" "16. parental pausa — bloquea X minutos\n" " ✏️ Ej: /parental pausa Carolina 60\n\n" "━━━ 🛠️ SISTEMA ━━━\n" "17. estado — estado de la Pi\n" " ✏️ Ej: /estado\n" "18. temperatura — temperatura de la Pi\n" " ✏️ Ej: /temperatura\n" "19. router estado|clientes|wifi|inventario|parental — controla el ASUS\n" " ✏️ Ej: /router estado\n" "20. panorama — fotos de las cámaras\n" " ✏️ Ej: /panorama\n" "21. limpieza — limpia procesos atascados\n" " ✏️ Ej: /limpieza\n" "22. servicios — estado de los servicios\n" " ✏️ Ej: /servicios\n" "23. reboot — reinicia la Pi\n" " ✏️ Ej: /reboot → confirmar con /confirmar-reboot\n" "24. ayuda — este índice\n" " ✏️ Ej: /ayuda\n\n" "━━━ 🌐 ROUTER ASUS — MESH / QoS / SEGURIDAD ━━━\n" "25. qos estado — ancho de banda con prioridades\n" " ✏️ Ej: /qos adaptive confirmar — auto gaming/streaming\n" "26. aiMesh estado — nodos, señal y roaming unificado\n" " ✏️ Ej: /aiMesh reiniciar nodo confirmar\n" "27. seguridad estado — AiProtection: filtro de webs\n" " ✏️ Ej: /seguridad bloquear tiktok\n" "28. limite listar — límite de velocidad por dispositivo\n" " ✏️ Ej: /limite poner NAS 50 20 — 50/20 Mbps", chat_id) elif t in ("servicios", "/servicios"): estado_svc = sh("systemctl is-active pi-bot tv-api autossh-tunel") nombres = "pi-bot\ntv-api\nautossh-tunel" activos = estado_svc.strip().splitlines() lineas = [] for n, e in zip(["pi-bot", "tv-api", "autossh-tunel"], activos): icono = "✅" if e == "active" else "❌" lineas.append(f"{icono} {n}: {e}") envio = "\n".join(lineas) enviar(f"📦 Servicios:\n{envio}\n\n🔌 Túnel VPS: {'OK' if activos and activos[2]=='active' else 'caído'}", chat_id) elif t == "reboot" or t == "/reboot": enviar("⚠️ Para reiniciar la Pi responde /confirmar-reboot en 30 segundos.", chat_id) elif t == "confirmar-reboot" or t == "/confirmar-reboot": enviar("🔄 Reiniciando la Pi... volveré en un momento.", chat_id) log("REBOOT solicitado") sh("echo 'raspberry' | sudo -S reboot") def procesar_series(chat_id): """Comando 'series': series con buena calidad en castellano del catalogo, con el comando 'descargar ' listo. Adultos efimeros (5 min).""" try: import pelis except Exception as e: log(f"series import error: {e}") enviar("❌ No se pudo cargar el modulo pelis", chat_id) return try: enviar("📺 Consultando series del catálogo...", chat_id) mensajes, adultos = pelis.generar_series() except Exception as e: log(f"series generar error: {e}") enviar(f"❌ Error consultando el catálogo: {e}", chat_id) return for m in mensajes: enviar(m, chat_id) if adultos: lineas = [f"🔞 Contenido adulto ({len(adultos)} novedades) — se borrará en 5 min:"] for it in adultos[:30]: y = f" ({it['year']})" if it["year"] else "" enlace = it.get("enlace") or "" linea = f"{it['clean']}{y}" if enlace: linea += f"\ndescargar {enlace}" lineas.append(linea) if len(adultos) > 30: lineas.append(f"... y {len(adultos) - 30} más") texto = "\n".join(lineas) for parte in pelis.trocear_efimero(texto, adultos): mid = enviar(parte, chat_id) if mid: threading.Timer(300, borrar, args=(chat_id, mid)).start() def procesar_pelis(chat_id): """Comando 'pelis': novedades con buena calidad en castellano, con el comando 'descargar ' listo para copiar. Los adultos se envian en un mensaje efimero que se borra a los 5 minutos y NO se loguea.""" try: import pelis except Exception as e: log(f"pelis import error: {e}") enviar("❌ No se pudo cargar el modulo pelis", chat_id) return try: enviar("🎬 Consultando novedades del catálogo...", chat_id) mensajes, adultos = pelis.generar() except Exception as e: log(f"pelis generar error: {e}") enviar(f"❌ Error consultando el catálogo: {e}", chat_id) return for m in mensajes: enviar(m, chat_id) # Adultos: efimeros (5 min), sin loguear, nunca persisten if adultos: lineas = [f"🔞 Contenido adulto ({len(adultos)} novedades) — se borrará en 5 min:"] for it in adultos[:30]: y = f" ({it['year']})" if it["year"] else "" enlace = it.get("enlace") or "" linea = f"{it['clean']}{y}" if enlace: linea += f"\ndescargar {enlace}" lineas.append(linea) if len(adultos) > 30: lineas.append(f"... y {len(adultos) - 30} más") texto = "\n".join(lineas) for parte in pelis.trocear_efimero(texto, adultos): mid = enviar(parte, chat_id) if mid: threading.Timer(300, borrar, args=(chat_id, mid)).start() def procesar_buscador_pelicula(nombre, chat_id): """Busca peliculas por nombre y muestra resultados numerados.""" try: sys.path.insert(0, os.path.join(BASE, "..", "videoclub")) import videoclub_buscador as vcb except Exception as e: log(f"videoclub_buscador import error: {e}") enviar("❌ No se pudo cargar el modulo videoclub", chat_id) return enviar(f"🔎 Buscando {nombre} en el catálogo...", chat_id) try: resultados = vcb.buscar_peliculas(nombre) mensajes = vcb.formatear_peliculas(resultados) except Exception as e: log(f"buscador pelicula error: {e}") enviar(f"❌ Error consultando el catálogo: {e}", chat_id) return for m in mensajes: enviar(m, chat_id) if resultados: log(f"BUSCADOR pelis '{nombre}': {len(resultados)} resultados") def procesar_buscador_serie(nombre, chat_id): """Busca series por nombre y muestra temporadas/capitulos numerados.""" try: sys.path.insert(0, os.path.join(BASE, "..", "videoclub")) import videoclub_buscador as vcb except Exception as e: log(f"videoclub_buscador import error: {e}") enviar("❌ No se pudo cargar el modulo videoclub", chat_id) return enviar(f"🔎 Buscando serie {nombre} en el catálogo...", chat_id) try: grupos = vcb.buscar_series(nombre) mensajes = vcb.formatear_series(grupos) except Exception as e: log(f"buscador serie error: {e}") enviar(f"❌ Error consultando el catálogo: {e}", chat_id) return for m in mensajes: enviar(m, chat_id) if grupos: total_eps = sum(len(v) for v in grupos.values()) log(f"BUSCADOR series '{nombre}': {len(grupos)} series, {total_eps} capitulos") def procesar_novedades(chat_id): """Muestra las ultimas novedades del catalogo.""" try: sys.path.insert(0, os.path.join(BASE, "..", "videoclub")) import videoclub_buscador as vcb except Exception as e: log(f"videoclub_buscador import error: {e}") enviar("❌ No se pudo cargar el modulo videoclub", chat_id) return enviar("🎬 Consultando ultimas novedades...", chat_id) try: items = vcb.generar_novedades() mensajes = vcb.formatear_novedades(items) except Exception as e: log(f"novedades error: {e}") enviar(f"❌ Error consultando el catálogo: {e}", chat_id) return for m in mensajes: enviar(m, chat_id) def _int_arg(texto): """Extrae el numero de un comando tipo '/bajar 3'.""" partes = texto.split() for p in partes[1:]: if p.isdigit(): return int(p) return None def _import_global_buscar(): sys.path.insert(0, os.path.join(BASE, "..", "videoclub")) import global_buscar as gol return gol def procesar_busca(nombre, chat_id): """Busqueda global (Cinemeta): texto -> titulos con id IMDb numerados.""" try: gol = _import_global_buscar() except Exception as e: log(f"global_buscar import error: {e}") enviar("❌ No se pudo cargar el modulo de busqueda global", chat_id) return enviar(f"🔎 Buscando {nombre} en todo el catalogo online...", chat_id) try: resultados = gol.buscar_titulos(nombre) mensajes = gol.formatear_titulos(resultados) except Exception as e: log(f"busca error: {e}") enviar(f"❌ Error en la busqueda: {e}", chat_id) return for m in mensajes: enviar(m, chat_id) if resultados: ultima_titulos[chat_id] = {"query": nombre, "results": resultados} log(f"BUSCA global '{nombre}': {len(resultados)} titulos") def procesar_genero(chat_id): """Comando '/genero': inicia el flujo por genero. Pide el tipo.""" flujo_genero[chat_id] = "esperando_tipo" flujo_genero_tipo.pop(chat_id, None) enviar("🎭 Catalogo por genero (30 mas descargadas). Primero elige el tipo:\n" "1. 🎬 Películas\n" "2. 📺 Series\n\nResponde el 1 o el 2.", chat_id) def _pedir_genero(chat_id, icono): try: gol = _import_global_buscar() except Exception as e: log(f"global_buscar import error: {e}") enviar("❌ No se pudo cargar el modulo", chat_id) return for m in gol.formatear_generos(): # anadimos el icono de tipo en la cabecera if m.startswith("🎭"): m = f"{icono} {m[1:]}" enviar(m, chat_id) enviar("Responde el numero del genero.", chat_id) def _mostrar_catalogo_genero(chat_id, tipo, genero, categoria): enviar(f"🔎 Consultando {genero} ({'peliculas' if tipo=='movie' else 'series'})...", chat_id) try: gol = _import_global_buscar() resultados = gol.catalogo_por_genero(tipo, genero, categoria, limite=30) mensajes = gol.formatear_catalogo(resultados, tipo, genero, categoria) except Exception as e: log(f"catalogo genero error: {e}") enviar(f"❌ Error consultando el catalogo: {e}", chat_id) return for m in mensajes: enviar(m, chat_id) if resultados: ultima_titulos[chat_id] = {"query": f"{genero} ({tipo})", "results": resultados} log(f"GENERO {tipo} '{genero}': {len(resultados)} titulos") def procesar_torrents(n, chat_id): """Muestra los torrents (Torrentio) del titulo numero n de la ultima busqueda.""" estado = ultima_titulos.get(chat_id) if not estado or n is None or n < 1 or n > len(estado["results"]): enviar("Primero usa /busca <nombre> y luego " "/torrents <numero>.", chat_id) return try: gol = _import_global_buscar() except Exception as e: log(f"global_buscar import error: {e}") enviar("❌ No se pudo cargar el modulo", chat_id) return r = estado["results"][n - 1] enviar(f"🧲 Buscando torrents de {r['name']}...", chat_id) try: torrents = gol.torrents_de(r["imdb_id"], r["type"]) torrents = gol.filtrar_torrents(torrents) mensajes = gol.formatear_torrents(torrents, r["name"]) except Exception as e: log(f"torrents error: {e}") enviar(f"❌ Error obteniendo torrents: {e}", chat_id) return for m in mensajes: enviar(m, chat_id) if torrents: ultimos_torrents[chat_id] = {"titulo": r["name"], "torrents": torrents} log(f"TORRENTS '{r['name']}': {len(torrents)} torrents") def procesar_bajar(n, chat_id): """Descarga el torrent numero n al NAS (Download Station via magnet).""" estado = ultimos_torrents.get(chat_id) if not estado or n is None or n < 1 or n > len(estado["torrents"]): enviar("Primero usa /busca <nombre> y luego " "/torrents <numero>.", chat_id) return try: gol = _import_global_buscar() except Exception as e: log(f"global_buscar import error: {e}") enviar("❌ No se pudo cargar el modulo", chat_id) return t = estado["torrents"][n - 1] magnet = gol.magnet_de(t) log(f"BAJAR '{estado['titulo']}' torrent {n}: {magnet[:60]}...") try: import nas_dl ok = nas_dl.add_torrent(magnet) except Exception as e: log(f"bajar error: {e}") enviar(f"❌ Error al anadir al NAS: {e}", chat_id) return if ok: tamaño = f"{t['size']:.1f}GB" if t.get("size") else "?" enviar(f"📥 Añadido al NAS: {t['filename'] or estado['titulo']} " f"({t['calidad']}, {tamaño}).", chat_id) else: enviar("❌ No se pudo añadir al NAS.", chat_id) def monitor_temperatura(): while True: time.sleep(300) msg, temp_c = estado() if temp_c is not None and temp_c > ALERTA_TEMP: enviar(f"🚨 ALERTA: la Pi está a {temp_c:.1f}°C " f"(límite {ALERTA_TEMP}°C)\n{msg}", CHAT_ID) def cargar_offset(): try: with open(OFFSET_FILE) as f: return int(f.read().strip()) except Exception: return None def guardar_offset(offset): try: with open(OFFSET_FILE, "w") as f: f.write(str(offset)) except Exception: pass def _atender_update(upd): """Procesa una update en su propio hilo (no bloquea el polling).""" try: if upd.get("callback_query"): procesar_callback(upd["callback_query"]) return msg = upd.get("message") or upd.get("channel_post") if not msg: return texto = msg.get("text") chat_id = msg["chat"]["id"] if texto: procesar(texto, chat_id) except Exception as e: log(f"ERROR update: {e}") def main(): if not TOKEN: print("Falta TELEGRAM_TOKEN en .env") return threading.Thread(target=monitor_temperatura, daemon=True).start() offset = cargar_offset() log("Bot iniciado" + (f" (offset {offset})" if offset else "")) print("Bot iniciado. Esperando mensajes...") while True: try: params = {"timeout": 25} if offset: params["offset"] = offset r = requests.get( f"https://api.telegram.org/bot{TOKEN}/getUpdates", params=params, timeout=35, ).json() for upd in r.get("result", []): offset = upd["update_id"] + 1 guardar_offset(offset) log(f"UPDATE {upd.get('update_id')}") threading.Thread(target=_atender_update, args=(upd,), daemon=True).start() except Exception as e: log(f"ERROR {e}") print("Error:", e) time.sleep(3) if __name__ == "__main__": main()