05-sep: [feat] menu wizard en bot OpenCode (/menu), menu IDD cierra al elegir + updates en hilos, borrar tarea NAS (0%) desde panel

This commit is contained in:
juanjo
2026-09-05 22:47:22 +02:00
parent 1c6bd113fd
commit 23fe44aecd
4 changed files with 342 additions and 25 deletions

View File

@@ -243,6 +243,24 @@ def _enviar_nuevo(chat_id, texto, teclado=None):
_log(f"send error: {e}")
def _cerrar_menu(chat_id, message_id):
"""Quita los botones del menu (lo deja cerrado como texto estatico)."""
import requests
try:
requests.post(_api("editMessageReplyMarkup"),
data={"chat_id": chat_id, "message_id": message_id},
timeout=10)
except Exception as e:
_log(f"cerrar menu error: {e}")
def _disp(chat_id, message_id, comando):
"""Ejecuta un comando del bot y cierra el menu (opcion elegida)."""
import pi_bot
pi_bot.procesar(comando, chat_id)
_cerrar_menu(chat_id, message_id)
# ----------------------------------------------------------------- menu ----
def enviar_menu(chat_id):
@@ -330,21 +348,23 @@ def procesar_callback(data, chat_id, message_id):
}
_enviar_nuevo(chat_id, preguntas.get(accion, "Escribe el dato:"),
teclado_cancelar())
_cerrar_menu(chat_id, message_id)
return
if ruta == "pausa_min":
nombre = _get_pausa_nombre(chat_id)
if not nombre:
_enviar_nuevo(chat_id, "Empieza de nuevo: /menu", teclado_cancelar())
_cerrar_menu(chat_id, message_id)
return
if partes[1] == "otro":
_guardar_pendiente(chat_id, "pausa_minutos")
_enviar_nuevo(chat_id, "⏱️ ¿Cuántos minutos? (1-1440)", teclado_cancelar())
_cerrar_menu(chat_id, message_id)
return
minutos = partes[1]
import pi_bot
_enviar_nuevo(chat_id, f"⏸️ Pausa {nombre} {minutos} minutos…", None)
pi_bot.procesar(f"parental pausa {nombre} {minutos}", chat_id)
_disp(chat_id, message_id,
f"parental pausa {nombre} {minutos}")
return
if ruta == "acc":
@@ -383,8 +403,7 @@ def _ejecutar_parental(chat_id, message_id, accion, nombre):
teclado_minutos_pausa(nombre))
return
comando = f"parental {'desactivar' if accion == 'desactivar' else 'activar'} {nombre}"
import pi_bot
pi_bot.procesar(comando, chat_id)
_disp(chat_id, message_id, comando)
def _ejecutar_accion(chat_id, message_id, partes):
@@ -408,10 +427,10 @@ def _ejecutar_accion(chat_id, message_id, partes):
"parental_listar": "parental listar",
}
if accion in directos:
pi_bot.procesar(directos[accion], chat_id)
_disp(chat_id, message_id, directos[accion])
return
if accion == "termo":
pi_bot.procesar(f"termo {sub}", chat_id)
_disp(chat_id, message_id, f"termo {sub}")
return
if accion == "router":
mapeo = {
@@ -423,12 +442,12 @@ def _ejecutar_accion(chat_id, message_id, partes):
"reiniciar": "router reiniciar",
"reservar": "router reservar",
}
pi_bot.procesar(mapeo.get(sub, "router ayuda"), chat_id)
_disp(chat_id, message_id, mapeo.get(sub, "router ayuda"))
return
if accion in ("torrents", "bajar"):
numero = int(sub) if sub.isdigit() else 0
if numero:
pi_bot.procesar(f"{accion} {numero}", chat_id)
_disp(chat_id, message_id, f"{accion} {numero}")
return

View File

@@ -210,6 +210,64 @@ def listar_descargas(hablar_solo_activas=True, session=None):
return lineas, activas
def listar_tareas(session=None):
"""Devuelve todas las tareas de Download Station con id y estado,
para poder borrar una concreta desde Telegram."""
if not session:
session = login()
if not session:
return []
try:
r = _api("/webapi/DownloadStation/task.cgi", {
"api": "SYNO.DownloadStation.Task",
"version": "3",
"method": "list",
"additional": "detail,transfer",
}, session=session)
if not r.get("success"):
return []
except Exception as e:
print(f"[NAS] Error listar_tareas: {e}")
return []
salida = []
for t in (r.get("data", {}).get("tasks", []) or []):
detail = (t.get("additional") or {}).get("detail") or {}
transfer = (t.get("additional") or {}).get("transfer") or {}
total = detail.get("total_pieces") or 0
done = transfer.get("downloaded_pieces") or 0
pct = (done * 100.0 / total) if total else 0.0
salida.append({
"id": t.get("id"),
"title": t.get("title") or "",
"status": t.get("status") or "?",
"pct": pct,
"size": transfer.get("size_downloaded") or 0,
})
return salida
def borrar_tarea(tid, session=None):
"""Borra una tarea de Download Station por su id.
Devuelve (ok, detalle)."""
if not session:
session = login()
if not session:
return False, "No se pudo autenticar con Download Station."
try:
import json as _json
r = _api("/webapi/DownloadStation/task.cgi", {
"api": "SYNO.DownloadStation.Task",
"version": "3",
"method": "delete",
"id": _json.dumps([tid]),
}, session=session)
except Exception as e:
return False, str(e)
if r.get("success"):
return True, "ok"
return False, str(r.get("error"))
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:

View File

@@ -19,6 +19,7 @@ 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")
@@ -119,6 +120,7 @@ def teclado_descargas():
],
[
{"text": "📁 Todas", "callback_data": "dl:all"},
{"text": "🗑️ Borrar…", "callback_data": "dl:del"},
{"text": "✖️ Cerrar", "callback_data": "dl:close"},
],
]
@@ -245,6 +247,9 @@ def procesar_callback(callback):
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":
@@ -292,6 +297,57 @@ def limpiar_pantalla(chat_id):
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 = ["🗑️ <b>Responde el número</b> de la tarea a BORRAR "
"(o escribe <code>cancelar</code>):", ""]
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: <b>{titulo[:80]}</b>", chat_id)
else:
enviar(f"❌ No se pudo borrar <b>{titulo[:60]}</b>: {detalle}", chat_id)
return True
def mandar_tv(mensaje, simple):
try:
url = f"{TV_API}?texto={urllib.parse.quote(mensaje)}"
@@ -340,6 +396,10 @@ def procesar(texto, 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]
@@ -418,6 +478,10 @@ def procesar(texto, 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":
@@ -716,7 +780,8 @@ def procesar(texto, chat_id):
" ✏️ Ej: <code>/novedades</code>\n\n"
"━━━ 🖥️ NAS / DESCARGAS ━━━\n"
"<b>8.</b> <code>descargas</code> — panel con progreso y botones\n"
" ✏️ Ej: <code>/descargas</code> o <code>/descargar</code>\n"
" ▶️ En el panel: botón <b>🗑️ Borrar…</b> para eliminar una tarea\n"
" ✏️ Ej: <code>/descargas</code> o <code>/borrar</code>\n"
"<b>9.</b> <code>descargar &lt;enlace&gt;</code> — añade a Download Station\n"
" ✏️ Ej: <code>/descargar magnet:?xt=urn:btih:...</code>\n"
"<b>10.</b> <code>termo on|off|estado</code> — controla el enchufe Tapo\n"
@@ -1091,6 +1156,23 @@ def guardar_offset(offset):
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")
@@ -1112,16 +1194,8 @@ def main():
offset = upd["update_id"] + 1
guardar_offset(offset)
log(f"UPDATE {upd.get('update_id')}")
if upd.get("callback_query"):
procesar_callback(upd["callback_query"])
continue
msg = upd.get("message") or upd.get("channel_post")
if not msg:
continue
texto = msg.get("text")
chat_id = msg["chat"]["id"]
if texto:
procesar(texto, chat_id)
threading.Thread(target=_atender_update, args=(upd,),
daemon=True).start()
except Exception as e:
log(f"ERROR {e}")
print("Error:", e)