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_PORT=465
|
||||||
SMTP_USER=your_email@example.com
|
SMTP_USER=your_email@example.com
|
||||||
SMTP_PASSWORD=your_password
|
SMTP_PASSWORD=your_password
|
||||||
IMAP_SERVER=imap.hostinger.com
|
SMTP_RECIPIENT=your_email@example.com # También se acepta PRINTER_EMAIL como alias
|
||||||
IMAP_PORT=993
|
|
||||||
IMAP_USER=your_email@example.com
|
|
||||||
IMAP_PASSWORD=your_password
|
|
||||||
PRINTER_EMAIL=your_email@example.com
|
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Copia el archivo `.env.example` a `.env` y rellena los valores correspondientes.
|
|||||||
TELEGRAM_TOKEN=TU_TOKEN_AQUI
|
TELEGRAM_TOKEN=TU_TOKEN_AQUI
|
||||||
|
|
||||||
# --- WEBHOOKS N8N ---
|
# --- WEBHOOKS N8N ---
|
||||||
WEBHOOK_ONBOARDING=https://...
|
WEBHOOK_CONTRATO=https://... # También acepta WEBHOOK_ONBOARDING
|
||||||
WEBHOOK_PRINT=https://...
|
WEBHOOK_PRINT=https://...
|
||||||
WEBHOOK_VACACIONES=https://...
|
WEBHOOK_VACACIONES=https://...
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ SMTP_SERVER=smtp.hostinger.com
|
|||||||
SMTP_PORT=465
|
SMTP_PORT=465
|
||||||
SMTP_USER=tu_email@dominio.com
|
SMTP_USER=tu_email@dominio.com
|
||||||
SMTP_PASSWORD=tu_password_de_email
|
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 ---
|
# --- 1. CARGA DE ENTORNO ---
|
||||||
load_dotenv() # Carga las variables del archivo .env
|
load_dotenv() # Carga las variables del archivo .env
|
||||||
TOKEN = os.getenv("TELEGRAM_TOKEN")
|
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
|
# Validación de seguridad
|
||||||
if not TOKEN:
|
if not TOKEN:
|
||||||
@@ -33,6 +31,14 @@ 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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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 ---
|
# --- 2. ESTADOS DEL FLUJO ---
|
||||||
(
|
(
|
||||||
NOMBRE_SALUDO, NOMBRE_COMPLETO, APELLIDO_PATERNO, APELLIDO_MATERNO,
|
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
|
siguiente_estado = estado_actual + 1
|
||||||
|
|
||||||
preguntas = {
|
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*?",
|
NOMBRE_COMPLETO: "¿Cuál es tu *apellido paterno*?",
|
||||||
APELLIDO_PATERNO: "¿Y tu *apellido materno*?",
|
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"}
|
headers = {"Content-Type": "application/json", "User-Agent": "Welcome2Soul-Bot"}
|
||||||
|
|
||||||
|
urls_a_enviar = WEBHOOK_URLS
|
||||||
enviado = False
|
enviado = False
|
||||||
for url in WEBHOOK_URLS:
|
for url in urls_a_enviar:
|
||||||
if not url: continue
|
if not url:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
res = requests.post(url.strip(), json=payload, headers=headers, timeout=20)
|
res = requests.post(url.strip(), json=payload, headers=headers, timeout=20)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
enviado = True
|
enviado = True
|
||||||
logging.info(f"Webhook enviado exitosamente a: {url}")
|
logging.info(f"Webhook enviado exitosamente a: {url}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error enviando webhook: {e}")
|
logging.error(f"Error enviando webhook a {url}: {e}")
|
||||||
|
|
||||||
if enviado:
|
if enviado:
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ from telegram import Update
|
|||||||
from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters
|
from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters
|
||||||
from modules.database import log_request
|
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 Configuration ---
|
||||||
SMTP_SERVER = os.getenv("SMTP_SERVER")
|
SMTP_SERVER = os.getenv("SMTP_SERVER")
|
||||||
SMTP_PORT = int(os.getenv("SMTP_PORT", 465))
|
SMTP_PORT = int(os.getenv("SMTP_PORT", 465))
|
||||||
SMTP_USER = os.getenv("SMTP_USER")
|
SMTP_USER = os.getenv("SMTP_USER")
|
||||||
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
|
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
|
# Estado
|
||||||
ESPERANDO_ARCHIVO = 1
|
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.")
|
await update.message.reply_text(f"Procesando *{file_name}*... un momento por favor.")
|
||||||
|
|
||||||
try:
|
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
|
# 1. Descargar el archivo de Telegram
|
||||||
file_info = await context.bot.get_file(file_id)
|
file_info = await context.bot.get_file(file_id)
|
||||||
file_url = file_info.file_path
|
file_url = file_info.file_path
|
||||||
@@ -74,7 +81,29 @@ async def recibir_archivo(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error al enviar correo: {e}") # Log para el admin
|
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.")
|
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
|
return ConversationHandler.END
|
||||||
|
|
||||||
async def cancelar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def cancelar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
|
|||||||
@@ -3,13 +3,38 @@ import re
|
|||||||
import requests
|
import requests
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
from telegram import Update
|
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
|
||||||
from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters
|
from telegram.ext import CommandHandler, ContextTypes, ConversationHandler, MessageHandler, filters
|
||||||
from modules.database import log_request
|
from modules.database import log_request
|
||||||
from modules.ai import classify_reason
|
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)
|
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:
|
def _calculate_vacation_metrics(date_string: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Calcula métricas de vacaciones a partir de un texto.
|
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
|
user = update.effective_user
|
||||||
log_request(user.id, user.username, "vacaciones", update.message.text)
|
log_request(user.id, user.username, "vacaciones", update.message.text)
|
||||||
context.user_data['tipo'] = 'VACACIONES'
|
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
|
return FECHAS
|
||||||
|
|
||||||
async def start_permiso(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def start_permiso(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
log_request(user.id, user.username, "permiso", update.message.text)
|
log_request(user.id, user.username, "permiso", update.message.text)
|
||||||
context.user_data['tipo'] = 'PERMISO'
|
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
|
return FECHAS
|
||||||
|
|
||||||
async def recibir_fechas(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
async def recibir_fechas(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||||
context.user_data['fechas'] = update.message.text
|
texto_fechas = update.message.text
|
||||||
await update.message.reply_text("Entendido. ¿Cuál es el motivo o comentario adicional?")
|
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
|
return MOTIVO
|
||||||
|
|
||||||
async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
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()),
|
"record_id": str(uuid.uuid4()),
|
||||||
"solicitante": {
|
"solicitante": {
|
||||||
"id_telegram": user.id,
|
"id_telegram": user.id,
|
||||||
"nombre": user.full_name
|
"nombre": user.full_name,
|
||||||
|
"username": user.username
|
||||||
},
|
},
|
||||||
"tipo_solicitud": datos['tipo'],
|
"tipo_solicitud": datos['tipo'],
|
||||||
"fechas_texto_original": datos['fechas'],
|
"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()
|
"created_at": datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
webhooks = []
|
||||||
if datos['tipo'] == 'PERMISO':
|
if datos['tipo'] == 'PERMISO':
|
||||||
webhook = os.getenv("WEBHOOK_PERMISOS")
|
webhooks = _get_webhook_list("WEBHOOK_PERMISOS")
|
||||||
categoria = classify_reason(motivo)
|
categoria = classify_reason(motivo)
|
||||||
payload["categoria_detectada"] = categoria
|
payload["categoria_detectada"] = categoria
|
||||||
await update.message.reply_text(f"Categoría detectada → **{categoria}** 🚨")
|
await update.message.reply_text(f"Categoría detectada → **{categoria}** 🚨")
|
||||||
|
|
||||||
elif datos['tipo'] == 'VACACIONES':
|
elif datos['tipo'] == 'VACACIONES':
|
||||||
webhook = os.getenv("WEBHOOK_VACACIONES")
|
webhooks = _get_webhook_list("WEBHOOK_VACACIONES")
|
||||||
metrics = _calculate_vacation_metrics(datos['fechas'])
|
metrics = datos.get('metricas_preliminares') or _calculate_vacation_metrics(datos['fechas'])
|
||||||
|
|
||||||
if metrics["dias_totales"] > 0:
|
if metrics["dias_totales"] > 0:
|
||||||
payload["metricas"] = metrics
|
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'.")
|
await update.message.reply_text("🤔 No entendí las fechas. Por favor, usa un formato como '10 al 15 de Octubre'.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if webhook:
|
enviados = _send_webhooks(webhooks, payload) if webhooks else 0
|
||||||
requests.post(webhook, json=payload)
|
tipo_solicitud_texto = "Permiso" if datos['tipo'] == 'PERMISO' else 'Vacaciones'
|
||||||
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.")
|
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:
|
except Exception as e:
|
||||||
print(f"Error enviando webhook: {e}")
|
print(f"Error enviando webhook: {e}")
|
||||||
await update.message.reply_text("⚠️ Error enviando la solicitud.")
|
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
|
# Handlers separados pero comparten lógica
|
||||||
vacaciones_handler = ConversationHandler(
|
vacaciones_handler = ConversationHandler(
|
||||||
entry_points=[CommandHandler("vacaciones", start_vacaciones)],
|
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)]
|
fallbacks=[CommandHandler("cancelar", cancelar)]
|
||||||
)
|
)
|
||||||
|
|
||||||
permiso_handler = ConversationHandler(
|
permiso_handler = ConversationHandler(
|
||||||
entry_points=[CommandHandler("permiso", start_permiso)],
|
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)]
|
fallbacks=[CommandHandler("cancelar", cancelar)]
|
||||||
)
|
)
|
||||||
@@ -4,3 +4,4 @@ requests
|
|||||||
SQLAlchemy
|
SQLAlchemy
|
||||||
mysql-connector-python
|
mysql-connector-python
|
||||||
google-generativeai
|
google-generativeai
|
||||||
|
openai
|
||||||
Reference in New Issue
Block a user