feat: Add Vikunja task management, refactor Google Calendar integration, and implement N8N webhook fallback.

This commit is contained in:
Marco Gallegos
2025-12-18 08:27:40 -06:00
parent cab2008838
commit 556fd8a3bd
12 changed files with 196 additions and 24 deletions

2
.gitignore vendored
View File

@@ -163,3 +163,5 @@ cython_debug/
# Logs # Logs
*.log *.log
# Test scripts
test_google_auth.py

View File

@@ -3,6 +3,12 @@
# Las variables de entorno son valores que se guardan fuera del código por seguridad (como tokens y llaves API). # Las variables de entorno son valores que se guardan fuera del código por seguridad (como tokens y llaves API).
import os import os
from dotenv import load_dotenv
from pathlib import Path
# Cargar variables de entorno desde el archivo .env en la raíz del proyecto
env_path = Path(__file__).parent.parent / '.env'
load_dotenv(dotenv_path=env_path)
# Token del bot de Telegram (obtenido de @BotFather) # Token del bot de Telegram (obtenido de @BotFather)
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
@@ -18,12 +24,19 @@ TEAM_CHAT_IDS = os.getenv("TEAM_CHAT_IDS", "").split(",")
# Ruta al archivo de credenciales de la cuenta de servicio de Google # Ruta al archivo de credenciales de la cuenta de servicio de Google
GOOGLE_SERVICE_ACCOUNT_FILE = os.getenv("GOOGLE_SERVICE_ACCOUNT_FILE") GOOGLE_SERVICE_ACCOUNT_FILE = os.getenv("GOOGLE_SERVICE_ACCOUNT_FILE")
if GOOGLE_SERVICE_ACCOUNT_FILE and not os.path.isabs(GOOGLE_SERVICE_ACCOUNT_FILE):
GOOGLE_SERVICE_ACCOUNT_FILE = str(Path(__file__).parent.parent / GOOGLE_SERVICE_ACCOUNT_FILE)
# ID del calendario de Google que usará el bot # ID del calendario de Google que usará el bot
CALENDAR_ID = os.getenv("CALENDAR_ID") CALENDAR_ID = os.getenv("CALENDAR_ID")
# URL del webhook de n8n para enviar datos a otros servicios # URL del webhook de n8n para enviar datos a otros servicios
N8N_WEBHOOK_URL = os.getenv("N8N_WEBHOOK_URL") N8N_WEBHOOK_URL = os.getenv("N8N_WEBHOOK_URL")
N8N_TEST_WEBHOOK_URL = os.getenv("N8N_TEST_WEBHOOK_URL")
# Configuración de Vikunja
VIKUNJA_API_URL = os.getenv("VIKUNJA_API_URL", "https://tasks.soul23.cloud/api/v1")
VIKUNJA_API_TOKEN = os.getenv("VIKUNJA_API_TOKEN")
# Llave de la API de OpenAI para usar modelos de lenguaje (como GPT) # Llave de la API de OpenAI para usar modelos de lenguaje (como GPT)
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

View File

@@ -1,4 +1,4 @@
# app/calendar.py # app/google_calendar.py
# Este script maneja la integración con Google Calendar (Calendario de Google). # Este script maneja la integración con Google Calendar (Calendario de Google).
# Permite buscar espacios libres y crear eventos. # Permite buscar espacios libres y crear eventos.

View File

@@ -15,7 +15,7 @@ from telegram.ext import (
# Importamos las configuraciones y herramientas que creamos en otros archivos # Importamos las configuraciones y herramientas que creamos en otros archivos
from config import TELEGRAM_BOT_TOKEN from config import TELEGRAM_BOT_TOKEN
from permissions import get_user_role from permissions import get_user_role, is_admin
from modules.onboarding import handle_start as onboarding_handle_start from modules.onboarding import handle_start as onboarding_handle_start
from modules.agenda import get_agenda from modules.agenda import get_agenda
from modules.citas import request_appointment from modules.citas import request_appointment
@@ -32,7 +32,8 @@ from modules.aprobaciones import view_pending, handle_approval_action
from modules.servicios import get_service_info from modules.servicios import get_service_info
from modules.admin import get_system_status from modules.admin import get_system_status
from modules.print import print_handler from modules.print import print_handler
from modules.create_tag import create_tag_conv_handler from modules.create_tag import create_tag_conv_handler, create_tag_start
from modules.vikunja import get_tasks
from scheduler import schedule_daily_summary from scheduler import schedule_daily_summary
# Configuramos el sistema de logs para ver mensajes de estado en la consola # Configuramos el sistema de logs para ver mensajes de estado en la consola
@@ -73,6 +74,7 @@ async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE)
# Diccionario de acciones simples (que solo devuelven texto) # Diccionario de acciones simples (que solo devuelven texto)
simple_handlers = { simple_handlers = {
'view_agenda': get_agenda, 'view_agenda': get_agenda,
'view_tasks': get_tasks,
'view_requests_status': view_requests_status, 'view_requests_status': view_requests_status,
'schedule_appointment': request_appointment, 'schedule_appointment': request_appointment,
'get_service_info': get_service_info, 'get_service_info': get_service_info,
@@ -93,6 +95,11 @@ async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE)
elif query.data.startswith(('approve:', 'reject:')): elif query.data.startswith(('approve:', 'reject:')):
# Manejo especial para botones de aprobar o rechazar # Manejo especial para botones de aprobar o rechazar
response_text = handle_approval_action(query.data) response_text = handle_approval_action(query.data)
elif query.data == 'start_create_tag':
# Iniciamos el flujo de creación de tag
await query.message.reply_text("Iniciando creación de tag...")
# Aquí simulamos el comando /create_tag
return await create_tag_start(update, context)
# Editamos el mensaje original con la nueva información # Editamos el mensaje original con la nueva información
await query.edit_message_text(text=response_text, reply_markup=reply_markup, parse_mode='Markdown') await query.edit_message_text(text=response_text, reply_markup=reply_markup, parse_mode='Markdown')
@@ -127,6 +134,15 @@ def main() -> None:
application.add_handler(create_tag_conv_handler()) application.add_handler(create_tag_conv_handler())
application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("print", print_handler)) 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)) application.add_handler(CallbackQueryHandler(button_dispatcher))
# Iniciamos el bot (se queda escuchando mensajes) # Iniciamos el bot (se queda escuchando mensajes)

View File

@@ -3,7 +3,7 @@
# Permite obtener y mostrar las actividades programadas para el día. # Permite obtener y mostrar las actividades programadas para el día.
import datetime import datetime
from calendar import get_events from google_calendar import get_events
def get_agenda(): def get_agenda():
""" """

View File

@@ -15,8 +15,9 @@ def get_owner_menu():
def get_admin_menu(): def get_admin_menu():
"""Crea el menú de botones para los Administradores.""" """Crea el menú de botones para los Administradores."""
keyboard = [ keyboard = [
[InlineKeyboardButton("📊 Ver estado del sistema", callback_data='view_system_status')], [InlineKeyboardButton("📋 Ver Tareas (Vikunja)", callback_data='view_tasks')],
[InlineKeyboardButton("👥 Gestionar usuarios", callback_data='manage_users')], [InlineKeyboardButton("🏷️ Crear Tag NFC", callback_data='start_create_tag')],
[InlineKeyboardButton("📊 Estado del sistema", callback_data='view_system_status')],
] ]
return InlineKeyboardMarkup(keyboard) return InlineKeyboardMarkup(keyboard)

View File

@@ -4,8 +4,8 @@
from telegram import Update from telegram import Update
from telegram.ext import ContextTypes from telegram.ext import ContextTypes
from ..permissions import is_admin from permissions import is_admin
from ..config import TIMEZONE, CALENDAR_ID, N8N_WEBHOOK_URL from config import TIMEZONE, CALENDAR_ID, N8N_WEBHOOK_URL
async def print_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def print_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
""" """

59
app/modules/vikunja.py Normal file
View File

@@ -0,0 +1,59 @@
# app/modules/vikunja.py
# Este módulo maneja la integración con Vikunja para la gestión de tareas.
import requests
import logging
from config import VIKUNJA_API_URL, VIKUNJA_API_TOKEN
logger = logging.getLogger(__name__)
def get_vikunja_headers():
"""Devuelve los headers necesarios para la API de Vikunja."""
return {
"Authorization": f"Bearer {VIKUNJA_API_TOKEN}",
"Content-Type": "application/json"
}
def get_tasks():
"""
Obtiene la lista de tareas desde Vikunja.
"""
if not VIKUNJA_API_TOKEN:
return "Error: VIKUNJA_API_TOKEN no configurado."
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.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"
return text
except Exception as e:
logger.error(f"Error al obtener tareas de Vikunja: {e}")
return f"Error al conectar con Vikunja: {e}"
def add_task(title):
"""
Agrega una nueva tarea a Vikunja.
"""
if not VIKUNJA_API_TOKEN:
return "Error: VIKUNJA_API_TOKEN no configurado."
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)
response.raise_for_status()
return f"✅ Tarea añadida: *{title}*"
except Exception as e:
logger.error(f"Error al añadir tarea a Vikunja: {e}")
return f"Error al añadir tarea: {e}"

View File

@@ -3,23 +3,31 @@
# En este caso, se comunica con n8n. # En este caso, se comunica con n8n.
import requests import requests
from config import N8N_WEBHOOK_URL from config import N8N_WEBHOOK_URL, N8N_TEST_WEBHOOK_URL
def send_webhook(event_data): def send_webhook(event_data):
""" """
Envía datos de un evento al servicio n8n. Envía datos de un evento al servicio n8n.
Usa el webhook normal y, si falla o no existe, usa el de test como fallback.
Parámetros:
- event_data: Un diccionario con la información que queremos enviar.
""" """
try: # Intentar con el webhook principal
# Hacemos una petición POST (enviar datos) a la URL configurada if N8N_WEBHOOK_URL:
response = requests.post(N8N_WEBHOOK_URL, json=event_data) try:
# Verificamos si la petición fue exitosa (status code 200-299) print(f"Intentando enviar a webhook principal: {N8N_WEBHOOK_URL}")
response.raise_for_status() response = requests.post(N8N_WEBHOOK_URL, json=event_data)
# Devolvemos la respuesta del servidor en formato JSON response.raise_for_status()
return response.json() return response.json()
except requests.exceptions.RequestException as e: except Exception as e:
# Si hay un error en la conexión o el envío, lo mostramos print(f"Fallo en webhook principal: {e}")
print(f"Error al enviar el webhook: {e}")
return None # Fallback al webhook de test
if N8N_TEST_WEBHOOK_URL:
try:
print(f"Intentando enviar a webhook de fallback (test): {N8N_TEST_WEBHOOK_URL}")
response = requests.post(N8N_TEST_WEBHOOK_URL, json=event_data)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"Fallo en webhook de fallback: {e}")
return None

View File

@@ -1,4 +1,4 @@
python-telegram-bot python-telegram-bot[job-queue]
requests requests
schedule schedule
google-api-python-client google-api-python-client
@@ -6,3 +6,4 @@ google-auth-httplib2
google-auth-oauthlib google-auth-oauthlib
openai openai
pytz pytz
python-dotenv

37
test_vikunja.py Normal file
View File

@@ -0,0 +1,37 @@
import os
import requests
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)
VIKUNJA_API_URL = os.getenv("VIKUNJA_API_URL", "https://tasks.soul23.cloud/api/v1")
VIKUNJA_API_TOKEN = os.getenv("VIKUNJA_API_TOKEN")
def test_vikunja_connection():
if not VIKUNJA_API_TOKEN:
print("Error: VIKUNJA_API_TOKEN no configurado en .env")
return
headers = {
"Authorization": f"Bearer {VIKUNJA_API_TOKEN}",
"Content-Type": "application/json"
}
print(f"Probando conexión a Vikunja: {VIKUNJA_API_URL}")
try:
# Intentar obtener información del usuario actual para validar el token
response = requests.get(f"{VIKUNJA_API_URL}/user", headers=headers)
if response.status_code == 200:
user_data = response.json()
print(f"¡Conexión exitosa! Usuario: {user_data.get('username')}")
else:
print(f"Error de conexión: {response.status_code} - {response.text}")
except Exception as e:
print(f"Error durante la prueba: {e}")
if __name__ == "__main__":
test_vikunja_connection()

35
vikunja.md Normal file
View File

@@ -0,0 +1,35 @@
# Vikunja Integration Flow (/vik)
## Objective
Implement a temporary flow controlled by the `/vik` command to manage tasks in Vikunja (https://tasks.soul23.cloud). This flow is exclusive to the Admin/Owner.
## Features
- **View Tasks**: List current tasks from Vikunja.
- **Add Task**: Remote task creation.
- **Edit Task**: Basic task modification.
## Technical Requirements
- **API Base**: `https://tasks.soul23.cloud/api/v1`
- **Authentication**: Bearer Token (to be configured in `.env`).
- **Access Control**: Only the Admin/Owner can trigger this flow.
## Webhook Fallback Logic
The system should implement a dual-webhook strategy:
1. **Primary**: `N8N_WEBHOOK_URL` (Normal/Production).
2. **Fallback**: `N8N_TEST_WEBHOOK_URL` (Test/Development).
If the primary webhook fails or is not configured, the system must attempt to use the fallback.
## Project Webhooks
Vikunja supports sending webhooks per project. This can be used to notify the bot (via n8n) when tasks are created or updated directly in the Vikunja interface, keeping the bot's context in sync.
---
> [!NOTE]
> This document serves as a specification for the development of the Vikunja module.