From d0879cc3d63c66377d7a088cc96c9e71526e81b8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:47:41 +0000 Subject: [PATCH] feat: Implement interactive Vikunja task management This commit introduces a `ConversationHandler` for the `/vik` command, replacing the previous simple command. The new implementation provides an interactive menu for users with admin permissions to: - View their tasks from a hardcoded project (ID 1). - Add new tasks to the same project. The changes include: - A new `ConversationHandler` in `app/modules/vikunja.py` to manage the interactive flow. - Integration of the new handler in `app/main.py`. - Removal of the old `/vik` command handler. - A fix in `test_vikunja.py` to correctly load environment variables. --- app/main.py | 12 +--- app/modules/vikunja.py | 122 +++++++++++++++++++++++++++++++---------- test_vikunja.py | 3 +- 3 files changed, 97 insertions(+), 40 deletions(-) diff --git a/app/main.py b/app/main.py index f3e412e..b2faf86 100644 --- a/app/main.py +++ b/app/main.py @@ -33,7 +33,7 @@ from modules.servicios import get_service_info from modules.admin import get_system_status from modules.print import print_handler from modules.create_tag import create_tag_conv_handler, create_tag_start -from modules.vikunja import get_tasks +from modules.vikunja import vikunja_conv_handler from scheduler import schedule_daily_summary # Configuramos el sistema de logs para ver mensajes de estado en la consola @@ -132,17 +132,9 @@ def main() -> None: # Registramos todos los manejadores de eventos en la aplicación application.add_handler(conv_handler) application.add_handler(create_tag_conv_handler()) + application.add_handler(vikunja_conv_handler()) application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("print", print_handler)) - - # Comando /vik restringido a administradores - async def vik_command_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): - if is_admin(update.effective_chat.id): - await update.message.reply_text(get_tasks(), parse_mode='Markdown') - else: - await update.message.reply_text("No tienes permiso para usar este comando.") - - application.add_handler(CommandHandler("vik", vik_command_handler)) application.add_handler(CallbackQueryHandler(button_dispatcher)) # Iniciamos el bot (se queda escuchando mensajes) diff --git a/app/modules/vikunja.py b/app/modules/vikunja.py index 980b227..0729d4e 100644 --- a/app/modules/vikunja.py +++ b/app/modules/vikunja.py @@ -3,57 +3,123 @@ import requests import logging -from config import VIKUNJA_API_URL, VIKUNJA_API_TOKEN +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import ( + ConversationHandler, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters, + ContextTypes, +) +from app.config import VIKUNJA_API_URL, VIKUNJA_API_TOKEN +from app.permissions import is_admin + +# Configuración del logger logger = logging.getLogger(__name__) +# Definición de los estados de la conversación +SELECTING_ACTION, ADDING_TASK = range(2) + def get_vikunja_headers(): """Devuelve los headers necesarios para la API de Vikunja.""" return { "Authorization": f"Bearer {VIKUNJA_API_TOKEN}", - "Content-Type": "application/json" + "Content-Type": "application/json", } -def get_tasks(): - """ - Obtiene la lista de tareas desde Vikunja. - """ +async def vik_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + """Inicia la conversación de Vikunja y muestra el menú de acciones.""" + if not is_admin(update.effective_chat.id): + await update.message.reply_text("No tienes permiso para usar este comando.") + return ConversationHandler.END + + keyboard = [ + [InlineKeyboardButton("Ver Tareas", callback_data='view_tasks')], + [InlineKeyboardButton("Añadir Tarea", callback_data='add_task')], + [InlineKeyboardButton("Cancelar", callback_data='cancel')], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text("Selecciona una opción para Vikunja:", reply_markup=reply_markup) + return SELECTING_ACTION + +async def view_tasks(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + """Muestra la lista de tareas de Vikunja.""" + query = update.callback_query + await query.answer() + if not VIKUNJA_API_TOKEN: - return "Error: VIKUNJA_API_TOKEN no configurado." + await query.edit_message_text("Error: VIKUNJA_API_TOKEN no configurado.") + return ConversationHandler.END try: - # Endpoint para obtener todas las tareas (ajustar según necesidad) - response = requests.get(f"{VIKUNJA_API_URL}/tasks/all", headers=get_vikunja_headers()) + response = requests.get(f"{VIKUNJA_API_URL}/projects/1/tasks", headers=get_vikunja_headers()) response.raise_for_status() tasks = response.json() if not tasks: - return "No tienes tareas pendientes en Vikunja." - - text = "📋 *Tus Tareas en Vikunja*\n\n" - for task in tasks[:10]: # Mostrar las primeras 10 - status = "✅" if task.get('done') else "⏳" - text += f"{status} *{task.get('title')}*\n" + text = "No tienes tareas pendientes en Vikunja." + else: + text = "📋 *Tus Tareas en Vikunja*\n\n" + for task in tasks[:10]: + status = "✅" if task.get('done') else "⏳" + text += f"{status} *{task.get('title')}*\n" - return text + await query.edit_message_text(text, parse_mode='Markdown') except Exception as e: logger.error(f"Error al obtener tareas de Vikunja: {e}") - return f"Error al conectar con Vikunja: {e}" + await query.edit_message_text(f"Error al conectar con Vikunja: {e}") + + return ConversationHandler.END + +async def request_task_title(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + """Solicita al usuario el título de la nueva tarea.""" + query = update.callback_query + await query.answer() + await query.edit_message_text("Por favor, introduce el título de la nueva tarea:") + return ADDING_TASK + +async def add_task(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + """Añade una nueva tarea a Vikunja.""" + task_title = update.message.text -def add_task(title): - """ - Agrega una nueva tarea a Vikunja. - """ if not VIKUNJA_API_TOKEN: - return "Error: VIKUNJA_API_TOKEN no configurado." + await update.message.reply_text("Error: VIKUNJA_API_TOKEN no configurado.") + return ConversationHandler.END try: - data = {"title": title} - # Nota: Vikunja suele requerir un project_id. Aquí usamos uno genérico o el primero disponible. - # Por ahora, este es un placeholder para el flujo /vik. - response = requests.put(f"{VIKUNJA_API_URL}/tasks", headers=get_vikunja_headers(), json=data) + # Usamos un project_id=1 hardcodeado como se definió en el plan + data = {"title": task_title, "project_id": 1} + response = requests.post(f"{VIKUNJA_API_URL}/tasks", headers=get_vikunja_headers(), json=data) response.raise_for_status() - return f"✅ Tarea añadida: *{title}*" + await update.message.reply_text(f"✅ Tarea añadida: *{task_title}*", parse_mode='Markdown') except Exception as e: logger.error(f"Error al añadir tarea a Vikunja: {e}") - return f"Error al añadir tarea: {e}" + await update.message.reply_text(f"Error al añadir tarea: {e}") + + return ConversationHandler.END + +async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + """Cancela la conversación.""" + query = update.callback_query + await query.answer() + await query.edit_message_text("Operación cancelada.") + return ConversationHandler.END + +def vikunja_conv_handler(): + """Crea el ConversationHandler para el flujo de Vikunja.""" + return ConversationHandler( + entry_points=[CommandHandler('vik', vik_start)], + states={ + SELECTING_ACTION: [ + CallbackQueryHandler(view_tasks, pattern='^view_tasks$'), + CallbackQueryHandler(request_task_title, pattern='^add_task$'), + CallbackQueryHandler(cancel, pattern='^cancel$'), + ], + ADDING_TASK: [ + MessageHandler(filters.TEXT & ~filters.COMMAND, add_task) + ], + }, + fallbacks=[CommandHandler('cancel', cancel)], + ) diff --git a/test_vikunja.py b/test_vikunja.py index bd1ec20..a2aeb32 100644 --- a/test_vikunja.py +++ b/test_vikunja.py @@ -5,8 +5,7 @@ from dotenv import load_dotenv from pathlib import Path # Cargar variables de entorno -env_path = Path(__file__).parent.parent / '.env' -load_dotenv(dotenv_path=env_path) +load_dotenv() VIKUNJA_API_URL = os.getenv("VIKUNJA_API_URL", "https://tasks.soul23.cloud/api/v1") VIKUNJA_API_TOKEN = os.getenv("VIKUNJA_API_TOKEN")