docs: Translate comments and logging messages to Spanish across various modules and the scheduler.

This commit is contained in:
Marco Gallegos
2025-12-18 00:17:14 -06:00
parent ade8a5f98d
commit e960538943
16 changed files with 266 additions and 230 deletions

View File

@@ -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

View File

@@ -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")

View File

@@ -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}"

View File

@@ -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()

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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)"

View File

@@ -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
) )

View File

@@ -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"

View File

@@ -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

View File

@@ -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.")

View File

@@ -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?"

View File

@@ -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"]

View File

@@ -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})")

View File

@@ -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