mirror of
https://github.com/marcogll/telegram_new_socias.git
synced 2026-01-13 13:15:16 +00:00
feat: Add print event webhooks and interactive keyboards for HR requests, refactor webhook handling, and remove unused IMAP configuration.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 MiB |
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
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)]
|
||||
)
|
||||
@@ -4,3 +4,4 @@ requests
|
||||
SQLAlchemy
|
||||
mysql-connector-python
|
||||
google-generativeai
|
||||
openai
|
||||
Reference in New Issue
Block a user