diff --git a/.env.example b/.env.example index bedd649..502c3c6 100644 --- a/.env.example +++ b/.env.example @@ -25,9 +25,4 @@ SMTP_SERVER=smtp.hostinger.com SMTP_PORT=465 SMTP_USER=your_email@example.com SMTP_PASSWORD=your_password -IMAP_SERVER=imap.hostinger.com -IMAP_PORT=993 -IMAP_USER=your_email@example.com -IMAP_PASSWORD=your_password -PRINTER_EMAIL=your_email@example.com - +SMTP_RECIPIENT=your_email@example.com # También se acepta PRINTER_EMAIL como alias diff --git a/Readme.md b/Readme.md index 08001e9..4881dbf 100644 --- a/Readme.md +++ b/Readme.md @@ -51,7 +51,7 @@ Copia el archivo `.env.example` a `.env` y rellena los valores correspondientes. TELEGRAM_TOKEN=TU_TOKEN_AQUI # --- WEBHOOKS N8N --- -WEBHOOK_ONBOARDING=https://... +WEBHOOK_CONTRATO=https://... # También acepta WEBHOOK_ONBOARDING WEBHOOK_PRINT=https://... WEBHOOK_VACACIONES=https://... @@ -68,7 +68,7 @@ SMTP_SERVER=smtp.hostinger.com SMTP_PORT=465 SMTP_USER=tu_email@dominio.com SMTP_PASSWORD=tu_password_de_email -SMTP_RECIPIENT=email_destino@dominio.com +SMTP_RECIPIENT=email_destino@dominio.com # También puedes usar PRINTER_EMAIL ``` --- diff --git a/Screenshot_20251214-112252.png b/Screenshot_20251214-112252.png deleted file mode 100644 index f8fd1dd..0000000 Binary files a/Screenshot_20251214-112252.png and /dev/null differ diff --git a/modules/onboarding.py b/modules/onboarding.py index a14a2a8..75aab13 100644 --- a/modules/onboarding.py +++ b/modules/onboarding.py @@ -22,8 +22,6 @@ from modules.database import log_request # --- 1. CARGA DE ENTORNO --- load_dotenv() # Carga las variables del archivo .env TOKEN = os.getenv("TELEGRAM_TOKEN") -# Convertimos la string del webhook en una lista (por si en el futuro hay varios separados por coma) -WEBHOOK_URLS = os.getenv("WEBHOOK_CONTRATO", "").split(",") # Validación de seguridad if not TOKEN: @@ -33,6 +31,14 @@ logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO ) +# Convertimos la string del webhook en una lista (por si en el futuro hay varios separados por coma) +# Se aceptan los nombres WEBHOOK_CONTRATO (nuevo) y WEBHOOK_ONBOARDING (legacy). +_webhook_raw = os.getenv("WEBHOOK_CONTRATO") or os.getenv("WEBHOOK_ONBOARDING") or "" +WEBHOOK_URLS = [w.strip() for w in _webhook_raw.split(",") if w.strip()] + +if not WEBHOOK_URLS: + logging.warning("No se configuró WEBHOOK_CONTRATO/WEBHOOK_ONBOARDING; el onboarding no enviará datos.") + # --- 2. ESTADOS DEL FLUJO --- ( NOMBRE_SALUDO, NOMBRE_COMPLETO, APELLIDO_PATERNO, APELLIDO_MATERNO, @@ -162,7 +168,7 @@ async def manejar_flujo(update: Update, context: ContextTypes.DEFAULT_TYPE, esta siguiente_estado = estado_actual + 1 preguntas = { - NOMBRE_SALUDO: "¡Lindo nombre! ✨\n\nNecesito tus datos oficiales para el contrato.\n¿Cuál es tu *nombre completo* (nombres) tal cual aparece en tu INE?", + NOMBRE_SALUDO: "¡Lindo nombre! ✨\n\nNecesito tus datos oficiales para el contrato.\n¿Cuáles son tus *nombres* (sin apellidos) tal cual aparecen en tu INE?", NOMBRE_COMPLETO: "¿Cuál es tu *apellido paterno*?", APELLIDO_PATERNO: "¿Y tu *apellido materno*?", @@ -290,16 +296,18 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: headers = {"Content-Type": "application/json", "User-Agent": "Welcome2Soul-Bot"} + urls_a_enviar = WEBHOOK_URLS enviado = False - for url in WEBHOOK_URLS: - if not url: continue + for url in urls_a_enviar: + if not url: + continue try: res = requests.post(url.strip(), json=payload, headers=headers, timeout=20) res.raise_for_status() enviado = True logging.info(f"Webhook enviado exitosamente a: {url}") except Exception as e: - logging.error(f"Error enviando webhook: {e}") + logging.error(f"Error enviando webhook a {url}: {e}") if enviado: await update.message.reply_text( diff --git a/modules/printer.py b/modules/printer.py index caeed33..58aad39 100644 --- a/modules/printer.py +++ b/modules/printer.py @@ -9,12 +9,16 @@ from telegram import Update from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters from modules.database import log_request +# Webhook opcional para notificar el evento de impresión +WEBHOOK_PRINTS = [w.strip() for w in (os.getenv("WEBHOOK_PRINT", "")).split(",") if w.strip()] + # --- SMTP Configuration --- SMTP_SERVER = os.getenv("SMTP_SERVER") SMTP_PORT = int(os.getenv("SMTP_PORT", 465)) SMTP_USER = os.getenv("SMTP_USER") SMTP_PASSWORD = os.getenv("SMTP_PASSWORD") -SMTP_RECIPIENT = os.getenv("SMTP_RECIPIENT") +# Permitimos PRINTER_EMAIL como alias legado para SMTP_RECIPIENT +SMTP_RECIPIENT = os.getenv("SMTP_RECIPIENT") or os.getenv("PRINTER_EMAIL") # Estado ESPERANDO_ARCHIVO = 1 @@ -35,6 +39,9 @@ async def recibir_archivo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> await update.message.reply_text(f"Procesando *{file_name}*... un momento por favor.") try: + if not all([SMTP_SERVER, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_RECIPIENT]): + raise RuntimeError("SMTP no configurado (falta SERVER/USER/PASSWORD/RECIPIENT).") + # 1. Descargar el archivo de Telegram file_info = await context.bot.get_file(file_id) file_url = file_info.file_path @@ -74,7 +81,29 @@ async def recibir_archivo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> except Exception as e: print(f"Error al enviar correo: {e}") # Log para el admin await update.message.reply_text("❌ Hubo un error al procesar tu archivo. Por favor, contacta a un administrador.") + return ConversationHandler.END + # Webhook de notificación (sin archivo, solo metadata) + if WEBHOOK_PRINTS: + payload = { + "accion": "PRINT", + "usuario": { + "id": user.id, + "username": user.username, + "nombre": user.full_name + }, + "archivo": { + "nombre": file_name, + "telegram_file_id": file_id, + }, + "enviado_via": "email", + "timestamp": update.message.date.isoformat() if update.message.date else None + } + for url in WEBHOOK_PRINTS: + try: + requests.post(url, json=payload, timeout=10).raise_for_status() + except Exception as e: + print(f"Error notificando webhook de impresión a {url}: {e}") return ConversationHandler.END async def cancelar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: diff --git a/modules/rh_requests.py b/modules/rh_requests.py index 7db1f43..05cb68b 100644 --- a/modules/rh_requests.py +++ b/modules/rh_requests.py @@ -3,13 +3,38 @@ import re import requests import uuid from datetime import datetime, date -from telegram import Update -from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters +from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update +from telegram.ext import CommandHandler, ContextTypes, ConversationHandler, MessageHandler, filters from modules.database import log_request from modules.ai import classify_reason +# Helpers de webhooks +def _get_webhook_list(env_name: str) -> list: + raw = os.getenv(env_name, "") + return [w.strip() for w in raw.split(",") if w.strip()] + +def _send_webhooks(urls: list, payload: dict): + enviados = 0 + for url in urls: + try: + res = requests.post(url, json=payload, timeout=15) + res.raise_for_status() + enviados += 1 + except Exception as e: + print(f"[webhook] Error enviando a {url}: {e}") + return enviados + TIPO_SOLICITITUD, FECHAS, MOTIVO = range(3) +# Teclados de apoyo +MESES = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"] +TECLADO_MESES = ReplyKeyboardMarkup([MESES[i:i+3] for i in range(0, 12, 3)], one_time_keyboard=True, resize_keyboard=True) +TECLADO_PERMISO_RAPIDO = ReplyKeyboardMarkup( + [["Hoy 09:00-11:00", "Hoy 15:00-18:00"], ["Mañana 09:00-11:00", "Mañana 15:00-18:00"], ["Otra fecha/horario"]], + one_time_keyboard=True, + resize_keyboard=True, +) + def _calculate_vacation_metrics(date_string: str) -> dict: """ Calcula métricas de vacaciones a partir de un texto. @@ -57,19 +82,39 @@ async def start_vacaciones(update: Update, context: ContextTypes.DEFAULT_TYPE) - user = update.effective_user log_request(user.id, user.username, "vacaciones", update.message.text) context.user_data['tipo'] = 'VACACIONES' - await update.message.reply_text("🌴 **Solicitud de Vacaciones**\n\n¿Para qué fechas las necesitas? (Ej: 10 al 15 de Octubre)") + await update.message.reply_text( + "🌴 **Solicitud de Vacaciones**\n\n¿Para qué fechas las necesitas?\nUsa el formato: `10 al 15 de Octubre`.", + reply_markup=TECLADO_MESES, + ) return FECHAS async def start_permiso(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: user = update.effective_user log_request(user.id, user.username, "permiso", update.message.text) context.user_data['tipo'] = 'PERMISO' - await update.message.reply_text("⏱️ **Solicitud de Permiso**\n\n¿Para qué día y horario lo necesitas?") + await update.message.reply_text( + "⏱️ **Solicitud de Permiso**\n\nSelecciona o escribe el día y horario. Ej: `Jueves 15 09:00-11:00`.", + reply_markup=TECLADO_PERMISO_RAPIDO, + ) return FECHAS async def recibir_fechas(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: - context.user_data['fechas'] = update.message.text - await update.message.reply_text("Entendido. ¿Cuál es el motivo o comentario adicional?") + texto_fechas = update.message.text + es_vacaciones = context.user_data.get('tipo') == 'VACACIONES' + + if es_vacaciones: + metrics = _calculate_vacation_metrics(texto_fechas) + if metrics["dias_totales"] == 0: + await update.message.reply_text( + "No entendí las fechas. Usa un formato como `10 al 15 de Octubre`.", + reply_markup=TECLADO_MESES, + ) + return FECHAS + # Si se entiende, guardamos también las métricas preliminares + context.user_data['metricas_preliminares'] = metrics + + context.user_data['fechas'] = texto_fechas + await update.message.reply_text("Entendido. ¿Cuál es el motivo o comentario adicional?", reply_markup=ReplyKeyboardRemove()) return MOTIVO async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: @@ -82,7 +127,8 @@ async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE) "record_id": str(uuid.uuid4()), "solicitante": { "id_telegram": user.id, - "nombre": user.full_name + "nombre": user.full_name, + "username": user.username }, "tipo_solicitud": datos['tipo'], "fechas_texto_original": datos['fechas'], @@ -90,15 +136,16 @@ async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE) "created_at": datetime.now().isoformat() } + webhooks = [] if datos['tipo'] == 'PERMISO': - webhook = os.getenv("WEBHOOK_PERMISOS") + webhooks = _get_webhook_list("WEBHOOK_PERMISOS") categoria = classify_reason(motivo) payload["categoria_detectada"] = categoria await update.message.reply_text(f"Categoría detectada → **{categoria}** 🚨") elif datos['tipo'] == 'VACACIONES': - webhook = os.getenv("WEBHOOK_VACACIONES") - metrics = _calculate_vacation_metrics(datos['fechas']) + webhooks = _get_webhook_list("WEBHOOK_VACACIONES") + metrics = datos.get('metricas_preliminares') or _calculate_vacation_metrics(datos['fechas']) if metrics["dias_totales"] > 0: payload["metricas"] = metrics @@ -122,10 +169,12 @@ async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE) await update.message.reply_text("🤔 No entendí las fechas. Por favor, usa un formato como '10 al 15 de Octubre'.") try: - if webhook: - requests.post(webhook, json=payload) - tipo_solicitud_texto = "Permiso" if datos['tipo'] == 'PERMISO' else 'Vacaciones' + enviados = _send_webhooks(webhooks, payload) if webhooks else 0 + tipo_solicitud_texto = "Permiso" if datos['tipo'] == 'PERMISO' else 'Vacaciones' + if enviados > 0: await update.message.reply_text(f"✅ Solicitud de *{tipo_solicitud_texto}* enviada a tu Manager.") + else: + await update.message.reply_text("⚠️ No hay webhook configurado o falló el envío. RH lo revisará.") except Exception as e: print(f"Error enviando webhook: {e}") await update.message.reply_text("⚠️ Error enviando la solicitud.") @@ -140,12 +189,18 @@ async def cancelar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: # Handlers separados pero comparten lógica vacaciones_handler = ConversationHandler( entry_points=[CommandHandler("vacaciones", start_vacaciones)], - states={FECHAS: [MessageHandler(filters.TEXT, recibir_fechas)], MOTIVO: [MessageHandler(filters.TEXT, recibir_motivo_fin)]}, + states={ + FECHAS: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_fechas)], + MOTIVO: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_motivo_fin)] + }, fallbacks=[CommandHandler("cancelar", cancelar)] ) permiso_handler = ConversationHandler( entry_points=[CommandHandler("permiso", start_permiso)], - states={FECHAS: [MessageHandler(filters.TEXT, recibir_fechas)], MOTIVO: [MessageHandler(filters.TEXT, recibir_motivo_fin)]}, + states={ + FECHAS: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_fechas)], + MOTIVO: [MessageHandler(filters.TEXT & ~filters.COMMAND, recibir_motivo_fin)] + }, fallbacks=[CommandHandler("cancelar", cancelar)] -) \ No newline at end of file +) diff --git a/requirements.txt b/requirements.txt index 7050dc8..46fc452 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ python-dotenv requests SQLAlchemy mysql-connector-python -google-generativeai \ No newline at end of file +google-generativeai +openai \ No newline at end of file