mirror of
https://github.com/marcogll/talia_bot.git
synced 2026-01-13 21:35:19 +00:00
docs: Translate comments and logging messages to Spanish across various modules and the scheduler.
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
# app/calendar.py
|
# app/calendar.py
|
||||||
|
# Este script maneja la integración con Google Calendar (Calendario de Google).
|
||||||
|
# Permite buscar espacios libres y crear eventos.
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from google.oauth2 import service_account
|
from google.oauth2 import service_account
|
||||||
@@ -6,11 +8,15 @@ from googleapiclient.discovery import build
|
|||||||
from googleapiclient.errors import HttpError
|
from googleapiclient.errors import HttpError
|
||||||
from config import GOOGLE_SERVICE_ACCOUNT_FILE, CALENDAR_ID
|
from config import GOOGLE_SERVICE_ACCOUNT_FILE, CALENDAR_ID
|
||||||
|
|
||||||
# Set up the Calendar API
|
# Configuración de los permisos (SCOPES) para acceder al calendario
|
||||||
SCOPES = ["https://www.googleapis.com/auth/calendar"]
|
SCOPES = ["https://www.googleapis.com/auth/calendar"]
|
||||||
|
|
||||||
|
# Autenticación usando el archivo de cuenta de servicio (Service Account)
|
||||||
creds = service_account.Credentials.from_service_account_file(
|
creds = service_account.Credentials.from_service_account_file(
|
||||||
GOOGLE_SERVICE_ACCOUNT_FILE, scopes=SCOPES
|
GOOGLE_SERVICE_ACCOUNT_FILE, scopes=SCOPES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Construcción del objeto 'service' que nos permite interactuar con la API de Google Calendar
|
||||||
service = build("calendar", "v3", credentials=creds)
|
service = build("calendar", "v3", credentials=creds)
|
||||||
|
|
||||||
|
|
||||||
@@ -18,12 +24,20 @@ def get_available_slots(
|
|||||||
start_time, end_time, duration_minutes=30, calendar_id=CALENDAR_ID
|
start_time, end_time, duration_minutes=30, calendar_id=CALENDAR_ID
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Fetches available calendar slots within a given time range.
|
Busca espacios disponibles en el calendario dentro de un rango de tiempo.
|
||||||
|
|
||||||
|
Parámetros:
|
||||||
|
- start_time: Hora de inicio de la búsqueda.
|
||||||
|
- end_time: Hora de fin de la búsqueda.
|
||||||
|
- duration_minutes: Cuánto dura cada cita (por defecto 30 min).
|
||||||
|
- calendar_id: El ID del calendario donde buscar.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Convertimos las fechas a formato ISO (el que entiende Google)
|
||||||
time_min = start_time.isoformat()
|
time_min = start_time.isoformat()
|
||||||
time_max = end_time.isoformat()
|
time_max = end_time.isoformat()
|
||||||
|
|
||||||
|
# Consultamos a Google qué horas están ocupadas (freebusy)
|
||||||
freebusy_query = {
|
freebusy_query = {
|
||||||
"timeMin": time_min,
|
"timeMin": time_min,
|
||||||
"timeMax": time_max,
|
"timeMax": time_max,
|
||||||
@@ -34,7 +48,7 @@ def get_available_slots(
|
|||||||
freebusy_result = service.freebusy().query(body=freebusy_query).execute()
|
freebusy_result = service.freebusy().query(body=freebusy_query).execute()
|
||||||
busy_slots = freebusy_result["calendars"][calendar_id]["busy"]
|
busy_slots = freebusy_result["calendars"][calendar_id]["busy"]
|
||||||
|
|
||||||
# Create a list of all potential slots
|
# Creamos una lista de todos los posibles espacios (slots)
|
||||||
potential_slots = []
|
potential_slots = []
|
||||||
current_time = start_time
|
current_time = start_time
|
||||||
while current_time + datetime.timedelta(minutes=duration_minutes) <= end_time:
|
while current_time + datetime.timedelta(minutes=duration_minutes) <= end_time:
|
||||||
@@ -44,15 +58,17 @@ def get_available_slots(
|
|||||||
current_time + datetime.timedelta(minutes=duration_minutes),
|
current_time + datetime.timedelta(minutes=duration_minutes),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# Avanzamos el tiempo para el siguiente espacio
|
||||||
current_time += datetime.timedelta(minutes=duration_minutes)
|
current_time += datetime.timedelta(minutes=duration_minutes)
|
||||||
|
|
||||||
# Filter out busy slots
|
# Filtramos los espacios que chocan con horas ocupadas
|
||||||
available_slots = []
|
available_slots = []
|
||||||
for slot_start, slot_end in potential_slots:
|
for slot_start, slot_end in potential_slots:
|
||||||
is_busy = False
|
is_busy = False
|
||||||
for busy in busy_slots:
|
for busy in busy_slots:
|
||||||
busy_start = datetime.datetime.fromisoformat(busy["start"])
|
busy_start = datetime.datetime.fromisoformat(busy["start"])
|
||||||
busy_end = datetime.datetime.fromisoformat(busy["end"])
|
busy_end = datetime.datetime.fromisoformat(busy["end"])
|
||||||
|
# Si el espacio propuesto se cruza con uno ocupado, lo marcamos como ocupado
|
||||||
if max(slot_start, busy_start) < min(slot_end, busy_end):
|
if max(slot_start, busy_start) < min(slot_end, busy_end):
|
||||||
is_busy = True
|
is_busy = True
|
||||||
break
|
break
|
||||||
@@ -61,14 +77,21 @@ def get_available_slots(
|
|||||||
|
|
||||||
return available_slots
|
return available_slots
|
||||||
except HttpError as error:
|
except HttpError as error:
|
||||||
print(f"An error occurred: {error}")
|
print(f"Ocurrió un error con la API de Google: {error}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def create_event(summary, start_time, end_time, attendees, calendar_id=CALENDAR_ID):
|
def create_event(summary, start_time, end_time, attendees, calendar_id=CALENDAR_ID):
|
||||||
"""
|
"""
|
||||||
Creates a new event in the calendar.
|
Crea un nuevo evento (cita) en el calendario.
|
||||||
|
|
||||||
|
Parámetros:
|
||||||
|
- summary: Título del evento.
|
||||||
|
- start_time: Hora de inicio.
|
||||||
|
- end_time: Hora de fin.
|
||||||
|
- attendees: Lista de correos electrónicos de los asistentes.
|
||||||
"""
|
"""
|
||||||
|
# Definimos la estructura del evento según pide Google
|
||||||
event = {
|
event = {
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
"start": {
|
"start": {
|
||||||
@@ -82,10 +105,11 @@ def create_event(summary, start_time, end_time, attendees, calendar_id=CALENDAR_
|
|||||||
"attendees": [{"email": email} for email in attendees],
|
"attendees": [{"email": email} for email in attendees],
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
|
# Insertamos el evento en el calendario
|
||||||
created_event = (
|
created_event = (
|
||||||
service.events().insert(calendarId=calendar_id, body=event).execute()
|
service.events().insert(calendarId=calendar_id, body=event).execute()
|
||||||
)
|
)
|
||||||
return created_event
|
return created_event
|
||||||
except HttpError as error:
|
except HttpError as error:
|
||||||
print(f"An error occurred: {error}")
|
print(f"Ocurrió un error al crear el evento: {error}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,12 +1,32 @@
|
|||||||
# app/config.py
|
# app/config.py
|
||||||
|
# Este archivo se encarga de cargar todas las variables de entorno y configuraciones del bot.
|
||||||
|
# Las variables de entorno son valores que se guardan fuera del código por seguridad (como tokens y llaves API).
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
# Token del bot de Telegram (obtenido de @BotFather)
|
||||||
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||||
|
|
||||||
|
# ID de chat del dueño del bot (para recibir notificaciones importantes)
|
||||||
OWNER_CHAT_ID = os.getenv("OWNER_CHAT_ID")
|
OWNER_CHAT_ID = os.getenv("OWNER_CHAT_ID")
|
||||||
|
|
||||||
|
# IDs de chat de los administradores, separados por comas en el archivo .env
|
||||||
ADMIN_CHAT_IDS = os.getenv("ADMIN_CHAT_IDS", "").split(",")
|
ADMIN_CHAT_IDS = os.getenv("ADMIN_CHAT_IDS", "").split(",")
|
||||||
|
|
||||||
|
# IDs de chat del equipo de trabajo, separados por comas
|
||||||
TEAM_CHAT_IDS = os.getenv("TEAM_CHAT_IDS", "").split(",")
|
TEAM_CHAT_IDS = os.getenv("TEAM_CHAT_IDS", "").split(",")
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
# 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
|
||||||
N8N_WEBHOOK_URL = os.getenv("N8N_WEBHOOK_URL")
|
N8N_WEBHOOK_URL = os.getenv("N8N_WEBHOOK_URL")
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
# Zona horaria por defecto para el manejo de fechas y horas
|
||||||
TIMEZONE = os.getenv("TIMEZONE", "America/Mexico_City")
|
TIMEZONE = os.getenv("TIMEZONE", "America/Mexico_City")
|
||||||
|
|||||||
19
app/llm.py
19
app/llm.py
@@ -1,23 +1,34 @@
|
|||||||
# app/llm.py
|
# app/llm.py
|
||||||
|
# Este script se encarga de la comunicación con la inteligencia artificial de OpenAI.
|
||||||
|
|
||||||
import openai
|
import openai
|
||||||
from config import OPENAI_API_KEY
|
from config import OPENAI_API_KEY
|
||||||
|
|
||||||
def get_smart_response(prompt):
|
def get_smart_response(prompt):
|
||||||
"""
|
"""
|
||||||
Generates a smart response using the OpenAI API.
|
Genera una respuesta inteligente usando la API de OpenAI.
|
||||||
|
|
||||||
|
Parámetros:
|
||||||
|
- prompt: El texto o pregunta que le enviamos a la IA.
|
||||||
"""
|
"""
|
||||||
|
# Verificamos que tengamos la llave de la API configurada
|
||||||
if not OPENAI_API_KEY:
|
if not OPENAI_API_KEY:
|
||||||
return "Error: OpenAI API key is not configured."
|
return "Error: La llave de la API de OpenAI no está configurada."
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Creamos el cliente de OpenAI
|
||||||
client = openai.OpenAI(api_key=OPENAI_API_KEY)
|
client = openai.OpenAI(api_key=OPENAI_API_KEY)
|
||||||
|
|
||||||
|
# Solicitamos una respuesta al modelo GPT-3.5-turbo
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model="gpt-3.5-turbo",
|
model="gpt-3.5-turbo",
|
||||||
messages=[
|
messages=[
|
||||||
{"role": "system", "content": "You are a helpful assistant."},
|
{"role": "system", "content": "Eres un asistente útil."},
|
||||||
{"role": "user", "content": prompt},
|
{"role": "user", "content": prompt},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
# Devolvemos el contenido de la respuesta limpia (sin espacios extras)
|
||||||
return response.choices[0].message.content.strip()
|
return response.choices[0].message.content.strip()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"An error occurred while communicating with OpenAI: {e}"
|
# Si algo sale mal, devolvemos el error
|
||||||
|
return f"Ocurrió un error al comunicarse con OpenAI: {e}"
|
||||||
|
|||||||
48
app/main.py
48
app/main.py
@@ -1,4 +1,6 @@
|
|||||||
# app/main.py
|
# app/main.py
|
||||||
|
# Este es el archivo principal del bot. Aquí se inicia todo y se configuran los comandos.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from telegram import Update
|
from telegram import Update
|
||||||
from telegram.ext import (
|
from telegram.ext import (
|
||||||
@@ -11,6 +13,7 @@ from telegram.ext import (
|
|||||||
filters,
|
filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
from modules.onboarding import handle_start as onboarding_handle_start
|
from modules.onboarding import handle_start as onboarding_handle_start
|
||||||
@@ -32,34 +35,42 @@ from modules.print import print_handler
|
|||||||
from modules.create_tag import create_tag_conv_handler
|
from modules.create_tag import create_tag_conv_handler
|
||||||
from scheduler import schedule_daily_summary
|
from scheduler import schedule_daily_summary
|
||||||
|
|
||||||
# Enable logging
|
# Configuramos el sistema de logs para ver mensajes de estado en la consola
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Sends a welcome message and menu when the /start command is issued."""
|
"""
|
||||||
|
Se ejecuta cuando el usuario escribe /start.
|
||||||
|
Muestra un mensaje de bienvenida y un menú según el rol del usuario.
|
||||||
|
"""
|
||||||
chat_id = update.effective_chat.id
|
chat_id = update.effective_chat.id
|
||||||
user_role = get_user_role(chat_id)
|
user_role = get_user_role(chat_id)
|
||||||
|
|
||||||
logger.info(f"User {chat_id} started conversation with role: {user_role}")
|
logger.info(f"Usuario {chat_id} inició conversación con el rol: {user_role}")
|
||||||
|
|
||||||
|
# Obtenemos el texto y los botones de bienvenida desde el módulo de onboarding
|
||||||
response_text, reply_markup = onboarding_handle_start(user_role)
|
response_text, reply_markup = onboarding_handle_start(user_role)
|
||||||
|
|
||||||
|
# Respondemos al usuario
|
||||||
await update.message.reply_text(response_text, reply_markup=reply_markup)
|
await update.message.reply_text(response_text, reply_markup=reply_markup)
|
||||||
|
|
||||||
async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Parses the CallbackQuery and routes it to the appropriate handler."""
|
"""
|
||||||
|
Esta función maneja los clics en los botones del menú.
|
||||||
|
Dependiendo de qué botón se presione, ejecuta una acción diferente.
|
||||||
|
"""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer() # Avisa a Telegram que recibimos el clic
|
||||||
logger.info(f"Dispatcher received callback query: {query.data}")
|
logger.info(f"El despachador recibió una consulta: {query.data}")
|
||||||
|
|
||||||
# Default response if no handler is found
|
# Texto por defecto si no encontramos la acción
|
||||||
response_text = "Acción no reconocida."
|
response_text = "Acción no reconocida."
|
||||||
reply_markup = None
|
reply_markup = None
|
||||||
|
|
||||||
# Simple callbacks that return a string
|
# Diccionario de acciones simples (que solo devuelven texto)
|
||||||
simple_handlers = {
|
simple_handlers = {
|
||||||
'view_agenda': get_agenda,
|
'view_agenda': get_agenda,
|
||||||
'view_requests_status': view_requests_status,
|
'view_requests_status': view_requests_status,
|
||||||
@@ -69,32 +80,38 @@ async def button_dispatcher(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
'manage_users': lambda: "Función de gestión de usuarios no implementada.",
|
'manage_users': lambda: "Función de gestión de usuarios no implementada.",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Callbacks that return a tuple (text, reply_markup)
|
# Diccionario de acciones complejas (que devuelven texto y botones)
|
||||||
complex_handlers = {
|
complex_handlers = {
|
||||||
'view_pending': view_pending,
|
'view_pending': view_pending,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Buscamos qué función ejecutar según el dato del botón (query.data)
|
||||||
if query.data in simple_handlers:
|
if query.data in simple_handlers:
|
||||||
response_text = simple_handlers[query.data]()
|
response_text = simple_handlers[query.data]()
|
||||||
elif query.data in complex_handlers:
|
elif query.data in complex_handlers:
|
||||||
response_text, reply_markup = complex_handlers[query.data]()
|
response_text, reply_markup = complex_handlers[query.data]()
|
||||||
elif query.data.startswith(('approve:', 'reject:')):
|
elif query.data.startswith(('approve:', 'reject:')):
|
||||||
|
# Manejo especial para botones de aprobar o rechazar
|
||||||
response_text = handle_approval_action(query.data)
|
response_text = handle_approval_action(query.data)
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Start the bot."""
|
"""Función principal que arranca el bot."""
|
||||||
|
# Verificamos que tengamos el token del bot
|
||||||
if not TELEGRAM_BOT_TOKEN:
|
if not TELEGRAM_BOT_TOKEN:
|
||||||
logger.error("TELEGRAM_BOT_TOKEN is not set in the environment variables.")
|
logger.error("TELEGRAM_BOT_TOKEN no está configurado en las variables de entorno.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Creamos la aplicación del bot
|
||||||
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
|
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
|
||||||
|
|
||||||
# Schedule daily summary
|
# Programamos el resumen diario
|
||||||
schedule_daily_summary(application)
|
schedule_daily_summary(application)
|
||||||
|
|
||||||
# Conversation handler for proposing activities
|
# Configuramos un "manejador de conversación" para proponer actividades
|
||||||
|
# Esto permite que el bot haga varias preguntas seguidas (descripción, duración)
|
||||||
conv_handler = ConversationHandler(
|
conv_handler = ConversationHandler(
|
||||||
entry_points=[CallbackQueryHandler(propose_activity_start, pattern='^propose_activity$')],
|
entry_points=[CallbackQueryHandler(propose_activity_start, pattern='^propose_activity$')],
|
||||||
states={
|
states={
|
||||||
@@ -105,14 +122,17 @@ def main() -> None:
|
|||||||
per_message=False
|
per_message=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Registramos todos los manejadores de eventos en la aplicación
|
||||||
application.add_handler(conv_handler)
|
application.add_handler(conv_handler)
|
||||||
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))
|
||||||
application.add_handler(CallbackQueryHandler(button_dispatcher))
|
application.add_handler(CallbackQueryHandler(button_dispatcher))
|
||||||
|
|
||||||
logger.info("Starting Talía Bot...")
|
# Iniciamos el bot (se queda escuchando mensajes)
|
||||||
|
logger.info("Iniciando Talía Bot...")
|
||||||
application.run_polling()
|
application.run_polling()
|
||||||
|
|
||||||
|
# Si este archivo se ejecuta directamente, llamamos a la función main()
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
# app/modules/admin.py
|
# app/modules/admin.py
|
||||||
"""
|
# Este módulo contiene funciones administrativas para el bot.
|
||||||
This module contains administrative functions for the bot.
|
# Por ahora, permite ver el estado general del sistema.
|
||||||
|
|
||||||
Currently, it provides a simple way to check the system's status.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_system_status():
|
def get_system_status():
|
||||||
"""
|
"""
|
||||||
Returns a formatted string with the current status of the bot and its integrations.
|
Devuelve un mensaje con el estado actual del bot y sus conexiones.
|
||||||
|
|
||||||
This function currently returns a hardcoded status message. In the future,
|
Actualmente el mensaje es fijo (hardcoded), pero en el futuro podría
|
||||||
it could be expanded to perform real-time checks on the different services.
|
hacer pruebas reales de conexión.
|
||||||
"""
|
"""
|
||||||
# TODO: Implement real-time status checks for more accurate monitoring.
|
# TODO: Implementar pruebas de estado en tiempo real para un monitoreo exacto.
|
||||||
status_text = (
|
status_text = (
|
||||||
"📊 *Estado del Sistema*\n\n"
|
"📊 *Estado del Sistema*\n\n"
|
||||||
"- *Bot Principal:* Activo ✅\n"
|
"- *Bot Principal:* Activo ✅\n"
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
# app/modules/agenda.py
|
# app/modules/agenda.py
|
||||||
"""
|
# Este módulo se encarga de manejar las peticiones relacionadas con la agenda.
|
||||||
This module is responsible for handling agenda-related requests.
|
# Permite obtener y mostrar las actividades programadas para el día.
|
||||||
|
|
||||||
It provides functionality to fetch and display the user's schedule for the day.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_agenda():
|
def get_agenda():
|
||||||
"""
|
"""
|
||||||
Fetches and displays the user's agenda for the current day.
|
Obtiene y muestra la agenda del usuario para el día actual.
|
||||||
|
|
||||||
Currently, this function returns a hardcoded sample agenda for demonstration
|
Por ahora, esta función devuelve una agenda de ejemplo fija.
|
||||||
purposes. The plan is to replace this with a real integration that fetches
|
El plan es conectarla con Google Calendar para que sea real.
|
||||||
events from a service like Google Calendar.
|
|
||||||
"""
|
"""
|
||||||
# TODO: Fetch the agenda dynamically from Google Calendar.
|
# TODO: Obtener la agenda dinámicamente desde Google Calendar.
|
||||||
agenda_text = (
|
agenda_text = (
|
||||||
"📅 *Agenda para Hoy*\n\n"
|
"📅 *Agenda para Hoy*\n\n"
|
||||||
"• *10:00 AM - 11:00 AM*\n"
|
"• *10:00 AM - 11:00 AM*\n"
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
# app/modules/aprobaciones.py
|
# app/modules/aprobaciones.py
|
||||||
"""
|
# Este módulo gestiona el flujo de aprobación para las solicitudes hechas por el equipo.
|
||||||
This module manages the approval workflow for requests made by the team.
|
# Permite ver solicitudes pendientes y aprobarlas o rechazarlas.
|
||||||
|
# El usuario principal aquí es el "owner" (dueño).
|
||||||
|
|
||||||
It provides functions to view pending requests and to handle the approval or
|
|
||||||
rejection of those requests. The primary user for this module is the "owner"
|
|
||||||
role, who has the authority to approve or deny requests.
|
|
||||||
"""
|
|
||||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
|
|
||||||
def get_approval_menu(request_id):
|
def get_approval_menu(request_id):
|
||||||
"""
|
"""
|
||||||
Creates and returns an inline keyboard with "Approve" and "Reject" buttons.
|
Crea un menú de botones (teclado en línea) con "Aprobar" y "Rechazar".
|
||||||
|
|
||||||
Each button is associated with a specific request_id through the
|
Cada botón lleva el ID de la solicitud para saber cuál estamos procesando.
|
||||||
callback_data, allowing the bot to identify which request is being acted upon.
|
|
||||||
"""
|
"""
|
||||||
keyboard = [
|
keyboard = [
|
||||||
[
|
[
|
||||||
|
# callback_data es lo que el bot recibe cuando se pulsa el botón
|
||||||
InlineKeyboardButton("✅ Aprobar", callback_data=f'approve:{request_id}'),
|
InlineKeyboardButton("✅ Aprobar", callback_data=f'approve:{request_id}'),
|
||||||
InlineKeyboardButton("❌ Rechazar", callback_data=f'reject:{request_id}'),
|
InlineKeyboardButton("❌ Rechazar", callback_data=f'reject:{request_id}'),
|
||||||
]
|
]
|
||||||
@@ -25,13 +22,11 @@ def get_approval_menu(request_id):
|
|||||||
|
|
||||||
def view_pending():
|
def view_pending():
|
||||||
"""
|
"""
|
||||||
Shows the owner a list of pending requests that require their attention.
|
Muestra al dueño una lista de solicitudes que esperan su aprobación.
|
||||||
|
|
||||||
Currently, this function uses a hardcoded list of proposals for demonstration.
|
Por ahora usa una lista fija de ejemplo.
|
||||||
In a production environment, this would fetch data from a database or another
|
|
||||||
persistent storage mechanism where pending requests are tracked.
|
|
||||||
"""
|
"""
|
||||||
# TODO: Fetch pending requests dynamically from a database or webhook events.
|
# TODO: Obtener solicitudes reales desde una base de datos o servicio externo.
|
||||||
proposals = [
|
proposals = [
|
||||||
{"id": "prop_001", "desc": "Grabación de proyecto", "duration": 4, "user": "Equipo A"},
|
{"id": "prop_001", "desc": "Grabación de proyecto", "duration": 4, "user": "Equipo A"},
|
||||||
{"id": "prop_002", "desc": "Taller de guion", "duration": 2, "user": "Equipo B"},
|
{"id": "prop_002", "desc": "Taller de guion", "duration": 2, "user": "Equipo B"},
|
||||||
@@ -40,7 +35,7 @@ def view_pending():
|
|||||||
if not proposals:
|
if not proposals:
|
||||||
return "No hay solicitudes pendientes.", None
|
return "No hay solicitudes pendientes.", None
|
||||||
|
|
||||||
# For demonstration purposes, we'll just show the first pending proposal.
|
# Tomamos la primera propuesta para mostrarla
|
||||||
proposal = proposals[0]
|
proposal = proposals[0]
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
@@ -50,28 +45,25 @@ def view_pending():
|
|||||||
f"⏳ *Duración:* {proposal['duration']} horas"
|
f"⏳ *Duración:* {proposal['duration']} horas"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Attach the approval menu to the message.
|
# Adjuntamos los botones de aprobación
|
||||||
reply_markup = get_approval_menu(proposal['id'])
|
reply_markup = get_approval_menu(proposal['id'])
|
||||||
|
|
||||||
return text, reply_markup
|
return text, reply_markup
|
||||||
|
|
||||||
def handle_approval_action(callback_data):
|
def handle_approval_action(callback_data):
|
||||||
"""
|
"""
|
||||||
Handles the owner's response (approve or reject) to a request.
|
Maneja la respuesta del dueño (clic en aprobar o rechazar).
|
||||||
|
|
||||||
This function is triggered when the owner clicks one of the buttons created
|
Separa la acción (approve/reject) del ID de la solicitud.
|
||||||
by get_approval_menu. It parses the callback_data to determine the action
|
|
||||||
and the request ID.
|
|
||||||
"""
|
"""
|
||||||
|
# callback_data viene como "accion:id", por ejemplo "approve:prop_001"
|
||||||
action, request_id = callback_data.split(':')
|
action, request_id = callback_data.split(':')
|
||||||
|
|
||||||
if action == 'approve':
|
if action == 'approve':
|
||||||
# TODO: Implement logic to update the request's status to 'approved'.
|
# TODO: Guardar en base de datos que fue aprobada y avisar al equipo.
|
||||||
# This could involve updating a database and notifying the requester.
|
|
||||||
return f"✅ La solicitud *{request_id}* ha sido aprobada."
|
return f"✅ La solicitud *{request_id}* ha sido aprobada."
|
||||||
elif action == 'reject':
|
elif action == 'reject':
|
||||||
# TODO: Implement logic to update the request's status to 'rejected'.
|
# TODO: Guardar en base de datos que fue rechazada y avisar al equipo.
|
||||||
# This could involve updating a database and notifying the requester.
|
|
||||||
return f"❌ La solicitud *{request_id}* ha sido rechazada."
|
return f"❌ La solicitud *{request_id}* ha sido rechazada."
|
||||||
|
|
||||||
return "Acción desconocida.", None
|
return "Acción desconocida.", None
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
# app/modules/citas.py
|
# app/modules/citas.py
|
||||||
"""
|
# Este módulo maneja la programación de citas para los clientes.
|
||||||
This module handles appointment scheduling for clients.
|
# Permite a los usuarios obtener un enlace para agendar una reunión.
|
||||||
|
|
||||||
It provides a simple way for users to get a link to an external scheduling
|
|
||||||
service, such as Calendly or an n8n workflow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def request_appointment():
|
def request_appointment():
|
||||||
"""
|
"""
|
||||||
Provides the user with a link to schedule an appointment.
|
Proporciona al usuario un enlace para agendar una cita.
|
||||||
|
|
||||||
Currently, this function returns a hardcoded placeholder link to Calendly.
|
Por ahora devuelve un enlace de ejemplo a Calendly.
|
||||||
The intention is to replace this with a dynamic link generated by an n8n
|
La idea es que sea un enlace dinámico generado por n8n.
|
||||||
workflow or another scheduling service.
|
|
||||||
"""
|
"""
|
||||||
# TODO: Integrate with a real scheduling service or an n8n workflow to
|
# TODO: Integrar con un servicio real o un flujo de n8n para dar un enlace personalizado.
|
||||||
# provide a dynamic and personalized scheduling link.
|
|
||||||
response_text = (
|
response_text = (
|
||||||
"Para agendar una cita, por favor utiliza el siguiente enlace: \n\n"
|
"Para agendar una cita, por favor utiliza el siguiente enlace: \n\n"
|
||||||
"[Enlace de Calendly](https://calendly.com/user/appointment-link)"
|
"[Enlace de Calendly](https://calendly.com/user/appointment-link)"
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
# app/modules/create_tag.py
|
# app/modules/create_tag.py
|
||||||
"""
|
# Este módulo permite crear un "tag" (etiqueta) con información del empleado.
|
||||||
This module contains the functionality for the /create_tag command.
|
# Usa un ConversationHandler para hacer una serie de preguntas al usuario.
|
||||||
|
# Al final, genera un código en Base64 que contiene toda la información en formato JSON.
|
||||||
|
|
||||||
It uses a ConversationHandler to guide the user through a series of questions
|
|
||||||
to collect data (name, employee number, branch, and Telegram ID), and then
|
|
||||||
generates a Base64-encoded JSON string from that data. This string is intended
|
|
||||||
to be used for creating an NFC tag.
|
|
||||||
"""
|
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -19,62 +15,56 @@ from telegram.ext import (
|
|||||||
filters,
|
filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Enable logging to monitor the bot's operation and for debugging.
|
# Configuramos los logs para este archivo
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Define the states for the conversation. These states are used to track the
|
# Definimos los estados de la conversación.
|
||||||
# user's progress through the conversation and determine which handler function
|
# Cada número representa un paso en el proceso de preguntas.
|
||||||
# should be executed next.
|
|
||||||
NAME, NUM_EMP, SUCURSAL, TELEGRAM_ID = range(4)
|
NAME, NUM_EMP, SUCURSAL, TELEGRAM_ID = range(4)
|
||||||
|
|
||||||
async def create_tag_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def create_tag_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
"""
|
"""
|
||||||
Starts the conversation to create a new tag when the /create_tag command
|
Inicia el proceso cuando el usuario escribe /create_tag.
|
||||||
is issued. It prompts the user for the first piece of information (name).
|
Pide el primer dato: el nombre.
|
||||||
"""
|
"""
|
||||||
await update.message.reply_text("Vamos a crear un nuevo tag. Por favor, dime el nombre:")
|
await update.message.reply_text("Vamos a crear un nuevo tag. Por favor, dime el nombre:")
|
||||||
# The function returns the next state, which is NAME, so the conversation
|
# Devolvemos el siguiente estado: NAME
|
||||||
# knows which handler to call next.
|
|
||||||
return NAME
|
return NAME
|
||||||
|
|
||||||
async def get_name(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def get_name(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
"""
|
"""
|
||||||
Stores the user's provided name in the context and then asks for the
|
Guarda el nombre y pide el número de empleado.
|
||||||
next piece of information, the employee number.
|
|
||||||
"""
|
"""
|
||||||
context.user_data['name'] = update.message.text
|
context.user_data['name'] = update.message.text
|
||||||
await update.message.reply_text("Gracias. Ahora, por favor, dime el número de empleado:")
|
await update.message.reply_text("Gracias. Ahora, por favor, dime el número de empleado:")
|
||||||
# The function returns the next state, NUM_EMP.
|
# Devolvemos el siguiente estado: NUM_EMP
|
||||||
return NUM_EMP
|
return NUM_EMP
|
||||||
|
|
||||||
async def get_num_emp(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def get_num_emp(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
"""
|
"""
|
||||||
Stores the employee number and proceeds to ask for the branch name.
|
Guarda el número de empleado y pide la sucursal.
|
||||||
"""
|
"""
|
||||||
context.user_data['num_emp'] = update.message.text
|
context.user_data['num_emp'] = update.message.text
|
||||||
await update.message.reply_text("Entendido. Ahora, por favor, dime la sucursal:")
|
await update.message.reply_text("Entendido. Ahora, por favor, dime la sucursal:")
|
||||||
# The function returns the next state, SUCURSAL.
|
# Devolvemos el siguiente estado: SUCURSAL
|
||||||
return SUCURSAL
|
return SUCURSAL
|
||||||
|
|
||||||
async def get_sucursal(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def get_sucursal(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
"""
|
"""
|
||||||
Stores the branch name and asks for the final piece of information,
|
Guarda la sucursal y pide el ID de Telegram.
|
||||||
the user's Telegram ID.
|
|
||||||
"""
|
"""
|
||||||
context.user_data['sucursal'] = update.message.text
|
context.user_data['sucursal'] = update.message.text
|
||||||
await update.message.reply_text("Perfecto. Finalmente, por favor, dime el ID de Telegram:")
|
await update.message.reply_text("Perfecto. Finalmente, por favor, dime el ID de Telegram:")
|
||||||
# The function returns the next state, TELEGRAM_ID.
|
# Devolvemos el siguiente estado: TELEGRAM_ID
|
||||||
return TELEGRAM_ID
|
return TELEGRAM_ID
|
||||||
|
|
||||||
async def get_telegram_id(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def get_telegram_id(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
"""
|
"""
|
||||||
Stores the Telegram ID, assembles all the collected data into a JSON
|
Guarda el ID de Telegram, junta todos los datos y genera el código Base64.
|
||||||
object, encodes it into a Base64 string, and sends the result back to
|
|
||||||
the user. This function concludes the conversation.
|
|
||||||
"""
|
"""
|
||||||
context.user_data['telegram_id'] = update.message.text
|
context.user_data['telegram_id'] = update.message.text
|
||||||
|
|
||||||
# Create a dictionary from the data collected and stored in user_data.
|
# Creamos un diccionario (como una caja con etiquetas) con todos los datos
|
||||||
tag_data = {
|
tag_data = {
|
||||||
"name": context.user_data.get('name'),
|
"name": context.user_data.get('name'),
|
||||||
"num_emp": context.user_data.get('num_emp'),
|
"num_emp": context.user_data.get('num_emp'),
|
||||||
@@ -82,28 +72,27 @@ async def get_telegram_id(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
"telegram_id": context.user_data.get('telegram_id'),
|
"telegram_id": context.user_data.get('telegram_id'),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Convert the Python dictionary into a JSON formatted string.
|
# Convertimos el diccionario a una cadena de texto en formato JSON
|
||||||
json_string = json.dumps(tag_data)
|
json_string = json.dumps(tag_data)
|
||||||
|
|
||||||
# Encode the JSON string into Base64. The string is first encoded to
|
# Convertimos esa cadena a Base64 (un formato que se puede guardar en tags NFC)
|
||||||
# UTF-8 bytes, which is then encoded to Base64 bytes, and finally
|
# 1. Codificamos a bytes (utf-8)
|
||||||
# decoded back to a UTF-8 string for display.
|
# 2. Codificamos esos bytes a base64
|
||||||
|
# 3. Convertimos de vuelta a texto para mostrarlo
|
||||||
base64_bytes = base64.b64encode(json_string.encode('utf-8'))
|
base64_bytes = base64.b64encode(json_string.encode('utf-8'))
|
||||||
base64_string = base64_bytes.decode('utf-8')
|
base64_string = base64_bytes.decode('utf-8')
|
||||||
|
|
||||||
await update.message.reply_text(f"¡Gracias! Aquí está tu tag en formato Base64:\n\n`{base64_string}`", parse_mode='Markdown')
|
await update.message.reply_text(f"¡Gracias! Aquí está tu tag en formato Base64:\n\n`{base64_string}`", parse_mode='Markdown')
|
||||||
|
|
||||||
# Clean up the user_data dictionary to ensure no data from this
|
# Limpiamos los datos temporales del usuario
|
||||||
# conversation is accidentally used in another one.
|
|
||||||
context.user_data.clear()
|
context.user_data.clear()
|
||||||
|
|
||||||
# End the conversation.
|
# Terminamos la conversación
|
||||||
return ConversationHandler.END
|
return ConversationHandler.END
|
||||||
|
|
||||||
async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
"""
|
"""
|
||||||
Cancels and ends the conversation if the user issues the /cancel command.
|
Cancela el proceso si el usuario escribe /cancel.
|
||||||
It also clears any data that has been collected so far.
|
|
||||||
"""
|
"""
|
||||||
await update.message.reply_text("Creación de tag cancelada.")
|
await update.message.reply_text("Creación de tag cancelada.")
|
||||||
context.user_data.clear()
|
context.user_data.clear()
|
||||||
@@ -111,19 +100,13 @@ async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|||||||
|
|
||||||
def create_tag_conv_handler():
|
def create_tag_conv_handler():
|
||||||
"""
|
"""
|
||||||
Creates and returns a ConversationHandler for the /create_tag command.
|
Configura el manejador de la conversación (el flujo de preguntas).
|
||||||
This handler manages the entire conversational flow, from starting the
|
|
||||||
conversation to handling user inputs and ending the conversation.
|
|
||||||
"""
|
"""
|
||||||
return ConversationHandler(
|
return ConversationHandler(
|
||||||
# The entry_points list defines how the conversation can be started.
|
# Punto de entrada: el comando /create_tag
|
||||||
# In this case, it's started by the /create_tag command.
|
|
||||||
entry_points=[CommandHandler('create_tag', create_tag_start)],
|
entry_points=[CommandHandler('create_tag', create_tag_start)],
|
||||||
|
|
||||||
# The states dictionary maps the conversation states to their
|
# Mapa de estados: qué función responde a cada paso
|
||||||
# respective handler functions. When the conversation is in a
|
|
||||||
# particular state, the corresponding handler is called to process
|
|
||||||
# the user's message.
|
|
||||||
states={
|
states={
|
||||||
NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_name)],
|
NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_name)],
|
||||||
NUM_EMP: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_num_emp)],
|
NUM_EMP: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_num_emp)],
|
||||||
@@ -131,12 +114,8 @@ def create_tag_conv_handler():
|
|||||||
TELEGRAM_ID: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_telegram_id)],
|
TELEGRAM_ID: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_telegram_id)],
|
||||||
},
|
},
|
||||||
|
|
||||||
# The fallbacks list defines handlers that are called if the user
|
# Si algo falla o el usuario cancela
|
||||||
# sends a message that doesn't match the current state's handler.
|
|
||||||
# Here, it's used to handle the /cancel command.
|
|
||||||
fallbacks=[CommandHandler('cancel', cancel)],
|
fallbacks=[CommandHandler('cancel', cancel)],
|
||||||
|
|
||||||
# per_message=False means the conversation is tied to the user, not
|
|
||||||
# to a specific message, which is standard for this type of flow.
|
|
||||||
per_message=False
|
per_message=False
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,45 +1,42 @@
|
|||||||
# app/modules/equipo.py
|
# app/modules/equipo.py
|
||||||
"""
|
# Este módulo contiene funciones para los miembros autorizados del equipo.
|
||||||
This module contains functionality for authorized team members.
|
# Incluye un flujo para proponer actividades que el dueño debe aprobar.
|
||||||
|
|
||||||
It includes a conversational flow for proposing new activities that require
|
|
||||||
approval from the owner, as well as a function to check the status of
|
|
||||||
previously submitted requests.
|
|
||||||
"""
|
|
||||||
from telegram import Update
|
from telegram import Update
|
||||||
from telegram.ext import ContextTypes, ConversationHandler
|
from telegram.ext import ContextTypes, ConversationHandler
|
||||||
|
|
||||||
# Define the states for the activity proposal conversation.
|
# Definimos los estados para la conversación de propuesta de actividad.
|
||||||
DESCRIPTION, DURATION = range(2)
|
DESCRIPTION, DURATION = range(2)
|
||||||
|
|
||||||
async def propose_activity_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def propose_activity_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
"""
|
"""
|
||||||
Starts the conversation for a team member to propose a new activity.
|
Inicia el proceso para que un miembro del equipo proponga una actividad.
|
||||||
This is typically triggered by an inline button press.
|
Se activa cuando se pulsa el botón correspondiente.
|
||||||
"""
|
"""
|
||||||
await update.callback_query.answer()
|
await update.callback_query.answer()
|
||||||
await update.callback_query.edit_message_text(
|
await update.callback_query.edit_message_text(
|
||||||
"Por favor, describe la actividad que quieres proponer."
|
"Por favor, describe la actividad que quieres proponer."
|
||||||
)
|
)
|
||||||
# The function returns the next state, which is DESCRIPTION.
|
# Siguiente paso: DESCRIPTION
|
||||||
return DESCRIPTION
|
return DESCRIPTION
|
||||||
|
|
||||||
async def get_description(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def get_description(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
"""
|
"""
|
||||||
Stores the activity description provided by the user and asks for the duration.
|
Guarda la descripción de la actividad y pide la duración.
|
||||||
"""
|
"""
|
||||||
context.user_data['activity_description'] = update.message.text
|
context.user_data['activity_description'] = update.message.text
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
"Entendido. Ahora, por favor, indica la duración estimada en horas (ej. 2, 4.5)."
|
"Entendido. Ahora, por favor, indica la duración estimada en horas (ej. 2, 4.5)."
|
||||||
)
|
)
|
||||||
# The function returns the next state, DURATION.
|
# Siguiente paso: DURATION
|
||||||
return DURATION
|
return DURATION
|
||||||
|
|
||||||
async def get_duration(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def get_duration(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
"""
|
"""
|
||||||
Stores the activity duration, confirms the proposal to the user, and ends the conversation.
|
Guarda la duración, confirma la propuesta y termina la conversación.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Intentamos convertir el texto a un número decimal (float)
|
||||||
duration = float(update.message.text)
|
duration = float(update.message.text)
|
||||||
context.user_data['activity_duration'] = duration
|
context.user_data['activity_duration'] = duration
|
||||||
description = context.user_data.get('activity_description', 'N/A')
|
description = context.user_data.get('activity_description', 'N/A')
|
||||||
@@ -51,24 +48,22 @@ async def get_duration(update: Update, context: ContextTypes.DEFAULT_TYPE) -> in
|
|||||||
"Recibirás una notificación cuando sea revisada."
|
"Recibirás una notificación cuando sea revisada."
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Send this proposal to the owner for approval, for example,
|
# TODO: Enviar esta propuesta al dueño (por webhook o base de datos).
|
||||||
# by sending a webhook or saving it to a database.
|
|
||||||
await update.message.reply_text(confirmation_text, parse_mode='Markdown')
|
await update.message.reply_text(confirmation_text, parse_mode='Markdown')
|
||||||
|
|
||||||
# Clean up user_data to prevent data leakage into other conversations.
|
# Limpiamos los datos temporales
|
||||||
context.user_data.clear()
|
context.user_data.clear()
|
||||||
|
|
||||||
# End the conversation.
|
# Terminamos la conversación
|
||||||
return ConversationHandler.END
|
return ConversationHandler.END
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# If the user provides an invalid number for the duration, ask again.
|
# Si el usuario no escribe un número válido, se lo pedimos de nuevo
|
||||||
await update.message.reply_text("Por favor, introduce un número válido para la duración en horas.")
|
await update.message.reply_text("Por favor, introduce un número válido para la duración en horas.")
|
||||||
return DURATION
|
return DURATION
|
||||||
|
|
||||||
async def cancel_proposal(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def cancel_proposal(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
"""
|
"""
|
||||||
Cancels and ends the activity proposal conversation.
|
Cancela el proceso de propuesta si el usuario escribe /cancel.
|
||||||
This is triggered by the /cancel command.
|
|
||||||
"""
|
"""
|
||||||
await update.message.reply_text("La propuesta de actividad ha sido cancelada.")
|
await update.message.reply_text("La propuesta de actividad ha sido cancelada.")
|
||||||
context.user_data.clear()
|
context.user_data.clear()
|
||||||
@@ -76,10 +71,9 @@ async def cancel_proposal(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
|
|
||||||
def view_requests_status():
|
def view_requests_status():
|
||||||
"""
|
"""
|
||||||
Allows a team member to see the status of their recent requests.
|
Permite a un miembro del equipo ver el estado de sus solicitudes recientes.
|
||||||
|
|
||||||
Currently, this returns a hardcoded sample status. In a real-world
|
Por ahora devuelve un estado de ejemplo fijo.
|
||||||
application, this would fetch the user's requests from a database.
|
|
||||||
"""
|
"""
|
||||||
# TODO: Fetch the status of recent requests from a persistent data source.
|
# TODO: Obtener el estado real desde una base de datos.
|
||||||
return "Aquí está el estado de tus solicitudes recientes:\n\n- Grabación de proyecto (4h): Aprobado\n- Taller de guion (2h): Pendiente"
|
return "Aquí está el estado de tus solicitudes recientes:\n\n- Grabación de proyecto (4h): Aprobado\n- Taller de guion (2h): Pendiente"
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
# app/modules/onboarding.py
|
# app/modules/onboarding.py
|
||||||
"""
|
# Este módulo maneja la primera interacción con el usuario (el comando /start).
|
||||||
This module handles the initial interaction with the user, specifically the
|
# Se encarga de mostrar un menú diferente según quién sea el usuario (dueño, admin, equipo o cliente).
|
||||||
/start command.
|
|
||||||
|
|
||||||
It is responsible for identifying the user's role and presenting them with a
|
|
||||||
customized menu of options based on their permissions. This ensures that each
|
|
||||||
user sees only the actions relevant to them.
|
|
||||||
"""
|
|
||||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
|
|
||||||
def get_owner_menu():
|
def get_owner_menu():
|
||||||
"""Creates and returns the main menu keyboard for the 'owner' role."""
|
"""Crea el menú de botones para el Dueño (Owner)."""
|
||||||
keyboard = [
|
keyboard = [
|
||||||
[InlineKeyboardButton("📅 Ver mi agenda", callback_data='view_agenda')],
|
[InlineKeyboardButton("📅 Ver mi agenda", callback_data='view_agenda')],
|
||||||
[InlineKeyboardButton("⏳ Ver pendientes", callback_data='view_pending')],
|
[InlineKeyboardButton("⏳ Ver pendientes", callback_data='view_pending')],
|
||||||
@@ -18,7 +13,7 @@ def get_owner_menu():
|
|||||||
return InlineKeyboardMarkup(keyboard)
|
return InlineKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
def get_admin_menu():
|
def get_admin_menu():
|
||||||
"""Creates and returns the main menu keyboard for the 'admin' role."""
|
"""Crea el menú de botones para los Administradores."""
|
||||||
keyboard = [
|
keyboard = [
|
||||||
[InlineKeyboardButton("📊 Ver estado del sistema", callback_data='view_system_status')],
|
[InlineKeyboardButton("📊 Ver estado del sistema", callback_data='view_system_status')],
|
||||||
[InlineKeyboardButton("👥 Gestionar usuarios", callback_data='manage_users')],
|
[InlineKeyboardButton("👥 Gestionar usuarios", callback_data='manage_users')],
|
||||||
@@ -26,7 +21,7 @@ def get_admin_menu():
|
|||||||
return InlineKeyboardMarkup(keyboard)
|
return InlineKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
def get_team_menu():
|
def get_team_menu():
|
||||||
"""Creates and returns the main menu keyboard for the 'team' role."""
|
"""Crea el menú de botones para los Miembros del Equipo."""
|
||||||
keyboard = [
|
keyboard = [
|
||||||
[InlineKeyboardButton("🕒 Proponer actividad", callback_data='propose_activity')],
|
[InlineKeyboardButton("🕒 Proponer actividad", callback_data='propose_activity')],
|
||||||
[InlineKeyboardButton("📄 Ver estatus de solicitudes", callback_data='view_requests_status')],
|
[InlineKeyboardButton("📄 Ver estatus de solicitudes", callback_data='view_requests_status')],
|
||||||
@@ -34,7 +29,7 @@ def get_team_menu():
|
|||||||
return InlineKeyboardMarkup(keyboard)
|
return InlineKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
def get_client_menu():
|
def get_client_menu():
|
||||||
"""Creates and returns the main menu keyboard for the 'client' role."""
|
"""Crea el menú de botones para los Clientes externos."""
|
||||||
keyboard = [
|
keyboard = [
|
||||||
[InlineKeyboardButton("🗓️ Agendar una cita", callback_data='schedule_appointment')],
|
[InlineKeyboardButton("🗓️ Agendar una cita", callback_data='schedule_appointment')],
|
||||||
[InlineKeyboardButton("ℹ️ Información de servicios", callback_data='get_service_info')],
|
[InlineKeyboardButton("ℹ️ Información de servicios", callback_data='get_service_info')],
|
||||||
@@ -43,20 +38,18 @@ def get_client_menu():
|
|||||||
|
|
||||||
def handle_start(user_role):
|
def handle_start(user_role):
|
||||||
"""
|
"""
|
||||||
Handles the /start command by sending a role-based welcome message and menu.
|
Decide qué mensaje y qué menú mostrar según el rol del usuario.
|
||||||
|
|
||||||
This function acts as a router, determining which menu to display based on
|
|
||||||
the user's role, which is passed in as an argument.
|
|
||||||
"""
|
"""
|
||||||
welcome_message = "Hola, soy Talía. ¿En qué puedo ayudarte hoy?"
|
welcome_message = "Hola, soy Talía. ¿En qué puedo ayudarte hoy?"
|
||||||
|
|
||||||
|
# Dependiendo del rol, llamamos a una función de menú diferente
|
||||||
if user_role == "owner":
|
if user_role == "owner":
|
||||||
menu = get_owner_menu()
|
menu = get_owner_menu()
|
||||||
elif user_role == "admin":
|
elif user_role == "admin":
|
||||||
menu = get_admin_menu()
|
menu = get_admin_menu()
|
||||||
elif user_role == "team":
|
elif user_role == "team":
|
||||||
menu = get_team_menu()
|
menu = get_team_menu()
|
||||||
else: # Default to the client menu for any other role.
|
else: # Por defecto, si no es ninguno de los anteriores, es un cliente
|
||||||
menu = get_client_menu()
|
menu = get_client_menu()
|
||||||
|
|
||||||
return welcome_message, menu
|
return welcome_message, menu
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
# app/modules/print.py
|
# app/modules/print.py
|
||||||
"""
|
# Este módulo permite a los administradores imprimir los detalles de configuración del bot.
|
||||||
This module provides a command for administrators to print out the current
|
# Es una herramienta útil para depuración (debugging).
|
||||||
configuration details of the bot.
|
|
||||||
|
|
||||||
It is a debugging and administrative tool that allows authorized users to quickly
|
|
||||||
inspect key configuration variables without accessing the environment directly.
|
|
||||||
"""
|
|
||||||
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
|
||||||
@@ -13,21 +9,22 @@ 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:
|
||||||
"""
|
"""
|
||||||
Handles the /print command.
|
Maneja el comando /print.
|
||||||
|
|
||||||
When triggered, this function first checks if the user has admin privileges.
|
Verifica si el usuario es administrador. Si lo es, muestra valores clave
|
||||||
If they do, it replies with a formatted message displaying the current values
|
de la configuración (Zona horaria, ID de calendario, Webhook).
|
||||||
of the TIMEZONE, CALENDAR_ID, and N8N_WEBHOOK_URL configuration variables.
|
|
||||||
If the user is not an admin, it sends a simple "not authorized" message.
|
|
||||||
"""
|
"""
|
||||||
chat_id = update.effective_chat.id
|
chat_id = update.effective_chat.id
|
||||||
|
|
||||||
|
# Solo permitimos esto a los administradores
|
||||||
if is_admin(chat_id):
|
if is_admin(chat_id):
|
||||||
config_details = (
|
config_details = (
|
||||||
f"**Configuration Details**\n"
|
f"**Detalles de Configuración**\n"
|
||||||
f"Timezone: `{TIMEZONE}`\n"
|
f"Zona Horaria: `{TIMEZONE}`\n"
|
||||||
f"Calendar ID: `{CALENDAR_ID}`\n"
|
f"ID de Calendario: `{CALENDAR_ID}`\n"
|
||||||
f"n8n Webhook URL: `{N8N_WEBHOOK_URL}`\n"
|
f"URL Webhook n8n: `{N8N_WEBHOOK_URL}`\n"
|
||||||
)
|
)
|
||||||
await update.message.reply_text(config_details, parse_mode='Markdown')
|
await update.message.reply_text(config_details, parse_mode='Markdown')
|
||||||
else:
|
else:
|
||||||
await update.message.reply_text("You are not authorized to use this command.")
|
# Si no es admin, le avisamos que no tiene permiso
|
||||||
|
await update.message.reply_text("No tienes autorización para usar este comando.")
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
# app/modules/servicios.py
|
# app/modules/servicios.py
|
||||||
"""
|
# Este módulo se encarga de dar información sobre los servicios ofrecidos.
|
||||||
This module is responsible for providing information about the services offered.
|
# Es un módulo informativo para los clientes.
|
||||||
|
|
||||||
It's a simple informational module that gives clients an overview of the
|
|
||||||
available services and can be expanded to provide more detailed information
|
|
||||||
or initiate a quoting process.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_service_info():
|
def get_service_info():
|
||||||
"""
|
"""
|
||||||
Provides a brief overview of the available services.
|
Muestra una lista breve de los servicios disponibles.
|
||||||
|
|
||||||
Currently, this function returns a hardcoded list of services. For a more
|
Por ahora devuelve un texto fijo. Se podría conectar a una base de datos
|
||||||
dynamic and easily maintainable system, this information could be fetched
|
para que sea más fácil de actualizar.
|
||||||
from a database, a configuration file, or an external API.
|
|
||||||
"""
|
"""
|
||||||
# TODO: Fetch service details from a database or a configuration file to
|
# TODO: Obtener detalles de servicios desde una base de datos o archivo de configuración.
|
||||||
# make the service list easier to manage and update.
|
|
||||||
return "Ofrecemos una variedad de servicios, incluyendo:\n\n- Consultoría Estratégica\n- Desarrollo de Software\n- Talleres de Capacitación\n\n¿Sobre cuál te gustaría saber más?"
|
return "Ofrecemos una variedad de servicios, incluyendo:\n\n- Consultoría Estratégica\n- Desarrollo de Software\n- Talleres de Capacitación\n\n¿Sobre cuál te gustaría saber más?"
|
||||||
|
|||||||
@@ -1,34 +1,39 @@
|
|||||||
# app/permissions.py
|
# app/permissions.py
|
||||||
|
# Este script maneja los permisos de los usuarios según su ID de chat de Telegram.
|
||||||
|
|
||||||
from config import OWNER_CHAT_ID, ADMIN_CHAT_IDS, TEAM_CHAT_IDS
|
from config import OWNER_CHAT_ID, ADMIN_CHAT_IDS, TEAM_CHAT_IDS
|
||||||
|
|
||||||
def get_user_role(chat_id):
|
def get_user_role(chat_id):
|
||||||
"""
|
"""
|
||||||
Determines the role of a user based on their chat ID.
|
Determina el rol de un usuario basado en su ID de chat.
|
||||||
|
|
||||||
|
Roles posibles: owner (dueño), admin (administrador), team (equipo), client (cliente).
|
||||||
"""
|
"""
|
||||||
chat_id_str = str(chat_id)
|
chat_id_str = str(chat_id)
|
||||||
|
|
||||||
|
# Si el ID coincide con el del dueño
|
||||||
if chat_id_str == OWNER_CHAT_ID:
|
if chat_id_str == OWNER_CHAT_ID:
|
||||||
return "owner"
|
return "owner"
|
||||||
|
|
||||||
|
# Si el ID está en la lista de administradores
|
||||||
if chat_id_str in ADMIN_CHAT_IDS:
|
if chat_id_str in ADMIN_CHAT_IDS:
|
||||||
return "admin"
|
return "admin"
|
||||||
|
|
||||||
|
# Si el ID está en la lista del equipo
|
||||||
if chat_id_str in TEAM_CHAT_IDS:
|
if chat_id_str in TEAM_CHAT_IDS:
|
||||||
return "team"
|
return "team"
|
||||||
|
|
||||||
|
# Si no es ninguno de los anteriores, es un cliente normal
|
||||||
return "client"
|
return "client"
|
||||||
|
|
||||||
def is_owner(chat_id):
|
def is_owner(chat_id):
|
||||||
"""
|
"""Verifica si un usuario es el dueño."""
|
||||||
Checks if a user is the owner.
|
|
||||||
"""
|
|
||||||
return get_user_role(chat_id) == "owner"
|
return get_user_role(chat_id) == "owner"
|
||||||
|
|
||||||
def is_admin(chat_id):
|
def is_admin(chat_id):
|
||||||
"""
|
"""Verifica si un usuario es administrador o dueño."""
|
||||||
Checks if a user is an admin.
|
|
||||||
"""
|
|
||||||
return get_user_role(chat_id) in ["owner", "admin"]
|
return get_user_role(chat_id) in ["owner", "admin"]
|
||||||
|
|
||||||
def is_team_member(chat_id):
|
def is_team_member(chat_id):
|
||||||
"""
|
"""Verifica si un usuario es parte del equipo, administrador o dueño."""
|
||||||
Checks if a user is a team member.
|
|
||||||
"""
|
|
||||||
return get_user_role(chat_id) in ["owner", "admin", "team"]
|
return get_user_role(chat_id) in ["owner", "admin", "team"]
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# app/scheduler.py
|
# app/scheduler.py
|
||||||
|
# Este script se encarga de programar tareas automáticas, como el resumen diario.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import time
|
from datetime import time
|
||||||
from telegram.ext import ContextTypes
|
from telegram.ext import ContextTypes
|
||||||
@@ -7,41 +9,51 @@ import pytz
|
|||||||
from config import OWNER_CHAT_ID, TIMEZONE
|
from config import OWNER_CHAT_ID, TIMEZONE
|
||||||
from modules.agenda import get_agenda
|
from modules.agenda import get_agenda
|
||||||
|
|
||||||
# Enable logging
|
# Configuramos el registro de eventos (logging) para ver qué pasa en la consola
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
async def send_daily_summary(context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def send_daily_summary(context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
"""Sends the daily summary to the owner."""
|
"""
|
||||||
|
Función que envía el resumen diario al dueño del bot.
|
||||||
|
Se ejecuta automáticamente según lo programado.
|
||||||
|
"""
|
||||||
job = context.job
|
job = context.job
|
||||||
chat_id = job.chat_id
|
chat_id = job.chat_id
|
||||||
|
|
||||||
logger.info(f"Running daily summary job for chat_id: {chat_id}")
|
logger.info(f"Ejecutando tarea de resumen diario para el chat_id: {chat_id}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Obtenemos la agenda del día
|
||||||
agenda_text = get_agenda()
|
agenda_text = get_agenda()
|
||||||
|
# Preparamos el mensaje
|
||||||
summary_text = f"🔔 *Resumen Diario - Buen día, Marco!*\n\n{agenda_text}"
|
summary_text = f"🔔 *Resumen Diario - Buen día, Marco!*\n\n{agenda_text}"
|
||||||
|
|
||||||
|
# Enviamos el mensaje por Telegram
|
||||||
await context.bot.send_message(
|
await context.bot.send_message(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
text=summary_text,
|
text=summary_text,
|
||||||
parse_mode='Markdown'
|
parse_mode='Markdown'
|
||||||
)
|
)
|
||||||
logger.info(f"Successfully sent daily summary to {chat_id}")
|
logger.info(f"Resumen diario enviado con éxito a {chat_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send daily summary to {chat_id}: {e}")
|
# Si hay un error, lo registramos
|
||||||
|
logger.error(f"Error al enviar el resumen diario a {chat_id}: {e}")
|
||||||
|
|
||||||
def schedule_daily_summary(application) -> None:
|
def schedule_daily_summary(application) -> None:
|
||||||
"""Schedules the daily summary job."""
|
"""
|
||||||
|
Programa la tarea del resumen diario para que ocurra todos los días.
|
||||||
|
"""
|
||||||
|
# Si no hay un ID de dueño configurado, no programamos nada
|
||||||
if not OWNER_CHAT_ID:
|
if not OWNER_CHAT_ID:
|
||||||
logger.warning("OWNER_CHAT_ID not set. Daily summary will not be scheduled.")
|
logger.warning("OWNER_CHAT_ID no configurado. No se programará el resumen diario.")
|
||||||
return
|
return
|
||||||
|
|
||||||
job_queue = application.job_queue
|
job_queue = application.job_queue
|
||||||
|
|
||||||
# Use the timezone from config
|
# Configuramos la zona horaria (ej. America/Mexico_City)
|
||||||
tz = pytz.timezone(TIMEZONE)
|
tz = pytz.timezone(TIMEZONE)
|
||||||
|
|
||||||
# Schedule the job to run every day at 7:00 AM
|
# Programamos la tarea para que corra todos los días a las 7:00 AM
|
||||||
scheduled_time = time(hour=7, minute=0, tzinfo=tz)
|
scheduled_time = time(hour=7, minute=0, tzinfo=tz)
|
||||||
|
|
||||||
job_queue.run_daily(
|
job_queue.run_daily(
|
||||||
@@ -51,4 +63,4 @@ def schedule_daily_summary(application) -> None:
|
|||||||
name="daily_summary"
|
name="daily_summary"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Scheduled daily summary for {OWNER_CHAT_ID} at {scheduled_time} {TIMEZONE}")
|
logger.info(f"Resumen diario programado para {OWNER_CHAT_ID} a las {scheduled_time} ({TIMEZONE})")
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
# app/webhook_client.py
|
# app/webhook_client.py
|
||||||
|
# Este script se encarga de enviar datos a servicios externos usando "webhooks".
|
||||||
|
# En este caso, se comunica con n8n.
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from config import N8N_WEBHOOK_URL
|
from config import N8N_WEBHOOK_URL
|
||||||
|
|
||||||
def send_webhook(event_data):
|
def send_webhook(event_data):
|
||||||
"""
|
"""
|
||||||
Sends a webhook to the n8n service.
|
Envía datos de un evento al servicio n8n.
|
||||||
|
|
||||||
|
Parámetros:
|
||||||
|
- event_data: Un diccionario con la información que queremos enviar.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Hacemos una petición POST (enviar datos) a la URL configurada
|
||||||
response = requests.post(N8N_WEBHOOK_URL, json=event_data)
|
response = requests.post(N8N_WEBHOOK_URL, json=event_data)
|
||||||
|
# Verificamos si la petición fue exitosa (status code 200-299)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
# Devolvemos la respuesta del servidor en formato JSON
|
||||||
return response.json()
|
return response.json()
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
print(f"Error sending webhook: {e}")
|
# Si hay un error en la conexión o el envío, lo mostramos
|
||||||
|
print(f"Error al enviar el webhook: {e}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user