mirror of
https://github.com/marcogll/talia_bot.git
synced 2026-01-13 13:25:19 +00:00
feat: Add Vikunja task management, refactor Google Calendar integration, and implement N8N webhook fallback.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -163,3 +163,5 @@ cython_debug/
|
|||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
# Test scripts
|
||||||
|
test_google_auth.py
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
20
app/main.py
20
app/main.py
@@ -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)
|
||||||
|
|||||||
@@ -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():
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
59
app/modules/vikunja.py
Normal 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}"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
37
test_vikunja.py
Normal 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
35
vikunja.md
Normal 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.
|
||||||
Reference in New Issue
Block a user