mirror of
https://github.com/marcogll/telegram_new_socias.git
synced 2026-01-13 13:15:16 +00:00
feat: Implement a new links menu, consolidate the main action keyboard into a ui module, and update employee ID generation logic.
This commit is contained in:
@@ -90,13 +90,13 @@ Si Collify solo consume imágenes ya publicadas, usa el archivo `docker-compose.
|
|||||||
|
|
||||||
1) Construir y publicar la imagen (ejemplo con Buildx y tag con timestamp):
|
1) Construir y publicar la imagen (ejemplo con Buildx y tag con timestamp):
|
||||||
```bash
|
```bash
|
||||||
export DOCKER_IMAGE=registry.example.com/vanessa-bot:prod-$(date +%Y%m%d%H%M)
|
export DOCKER_IMAGE=marcogll/vanessa-bot:prod-$(date +%Y%m%d%H%M)
|
||||||
docker buildx build --platform linux/amd64 -t $DOCKER_IMAGE . --push
|
docker buildx build --platform linux/amd64 -t $DOCKER_IMAGE . --push
|
||||||
```
|
```
|
||||||
|
|
||||||
2) Desplegar en el servidor (Collify) usando la imagen publicada:
|
2) Desplegar en el servidor (Collify) usando la imagen publicada:
|
||||||
```bash
|
```bash
|
||||||
export DOCKER_IMAGE=registry.example.com/vanessa-bot:prod-20240101
|
export DOCKER_IMAGE=marcogll/vanessa-bot:prod-20240101
|
||||||
docker compose -f docker-compose.collify.yml pull
|
docker compose -f docker-compose.collify.yml pull
|
||||||
docker compose -f docker-compose.collify.yml up -d
|
docker compose -f docker-compose.collify.yml up -d
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
bot:
|
bot:
|
||||||
image: ${DOCKER_IMAGE:-vanessa-bot:latest}
|
image: marcogll/vanessa-bot:latest
|
||||||
container_name: vanessa_bot
|
container_name: vanessa_bot
|
||||||
restart: always
|
restart: always
|
||||||
env_file:
|
env_file:
|
||||||
|
|||||||
67
main.py
67
main.py
@@ -1,11 +1,17 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
# Cargar variables de entorno antes de importar módulos que las usan
|
# Cargar variables de entorno antes de importar módulos que las usan
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
from telegram import Update, ReplyKeyboardMarkup, BotCommand
|
from telegram import (
|
||||||
|
Update,
|
||||||
|
InlineKeyboardButton,
|
||||||
|
InlineKeyboardMarkup,
|
||||||
|
BotCommand,
|
||||||
|
)
|
||||||
from telegram.constants import ParseMode
|
from telegram.constants import ParseMode
|
||||||
from telegram.ext import Application, Defaults, CommandHandler, ContextTypes
|
from telegram.ext import Application, Defaults, CommandHandler, ContextTypes
|
||||||
|
|
||||||
@@ -13,30 +19,67 @@ from telegram.ext import Application, Defaults, CommandHandler, ContextTypes
|
|||||||
from modules.onboarding import onboarding_handler
|
from modules.onboarding import onboarding_handler
|
||||||
from modules.rh_requests import vacaciones_handler, permiso_handler
|
from modules.rh_requests import vacaciones_handler, permiso_handler
|
||||||
from modules.database import log_request
|
from modules.database import log_request
|
||||||
|
from modules.ui import main_actions_keyboard
|
||||||
# from modules.finder import finder_handler (Si lo creas después)
|
# from modules.finder import finder_handler (Si lo creas después)
|
||||||
|
|
||||||
|
LINK_CURSOS = "https://cursos.vanityexperience.mx/dashboard-2/"
|
||||||
|
LINK_SITIO = "https://vanityexperience.mx/"
|
||||||
|
LINK_AGENDA_IOS = "https://apps.apple.com/us/app/fresha-for-business/id1455346253"
|
||||||
|
LINK_AGENDA_ANDROID = "https://play.google.com/store/apps/details?id=com.fresha.Business"
|
||||||
|
|
||||||
TOKEN = os.getenv("TELEGRAM_TOKEN")
|
TOKEN = os.getenv("TELEGRAM_TOKEN")
|
||||||
|
|
||||||
logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO)
|
logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO)
|
||||||
|
|
||||||
|
def _guess_platform(update: Update) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Telegram no expone el OS del usuario en mensajes regulares.
|
||||||
|
Devolvemos None para mostrar ambos links; si en el futuro llegan datos, se pueden mapear aquí.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
_ = update.to_dict() # placeholder por si queremos inspeccionar el payload
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def links_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Muestra accesos rápidos a cursos, sitio y descargas."""
|
||||||
|
user = update.effective_user
|
||||||
|
log_request(user.id, user.username, "links", update.message.text)
|
||||||
|
|
||||||
|
plataforma = _guess_platform(update)
|
||||||
|
descarga_buttons = []
|
||||||
|
if plataforma == "ios":
|
||||||
|
descarga_buttons.append(InlineKeyboardButton("Agenda | iOS", url=LINK_AGENDA_IOS))
|
||||||
|
elif plataforma == "android":
|
||||||
|
descarga_buttons.append(InlineKeyboardButton("Agenda | Android", url=LINK_AGENDA_ANDROID))
|
||||||
|
else:
|
||||||
|
descarga_buttons = [
|
||||||
|
InlineKeyboardButton("Agenda | iOS", url=LINK_AGENDA_IOS),
|
||||||
|
InlineKeyboardButton("Agenda | Android", url=LINK_AGENDA_ANDROID),
|
||||||
|
]
|
||||||
|
|
||||||
|
texto = (
|
||||||
|
"🌐 Links útiles\n"
|
||||||
|
"Claro, aquí tienes enlaces que puedes necesitar durante tu estancia con nosotros:\n"
|
||||||
|
"Toca el que te aplique."
|
||||||
|
)
|
||||||
|
botones = [
|
||||||
|
[InlineKeyboardButton("Cursos Vanity", url=LINK_CURSOS)],
|
||||||
|
[InlineKeyboardButton("Sitio Vanity", url=LINK_SITIO)],
|
||||||
|
descarga_buttons,
|
||||||
|
]
|
||||||
|
await update.message.reply_text(texto, reply_markup=InlineKeyboardMarkup(botones))
|
||||||
|
|
||||||
async def menu_principal(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def menu_principal(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Muestra el menú de opciones de Vanessa"""
|
"""Muestra el menú de opciones de Vanessa"""
|
||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
log_request(user.id, user.username, "start", update.message.text)
|
log_request(user.id, user.username, "start", update.message.text)
|
||||||
texto = (
|
texto = (
|
||||||
"👩💼 **Hola, soy Vanessa. ¿En qué puedo ayudarte hoy?**\n\n"
|
"👩💼 **Hola, soy Vanessa. ¿En qué puedo ayudarte hoy?**\n\n"
|
||||||
"Comandos rápidos:\n"
|
"Toca un botón para continuar 👇"
|
||||||
"/welcome — Registro de nuevas empleadas\n"
|
|
||||||
"/vacaciones — Solicitud de vacaciones\n"
|
|
||||||
"/permiso — Solicitud de permiso por horas\n\n"
|
|
||||||
"También tienes los botones rápidos abajo 👇"
|
|
||||||
)
|
)
|
||||||
teclado = ReplyKeyboardMarkup(
|
await update.message.reply_text(texto, reply_markup=main_actions_keyboard())
|
||||||
[["/welcome"], ["/vacaciones", "/permiso"]],
|
|
||||||
resize_keyboard=True
|
|
||||||
)
|
|
||||||
await update.message.reply_text(texto, reply_markup=teclado)
|
|
||||||
|
|
||||||
async def post_init(application: Application):
|
async def post_init(application: Application):
|
||||||
# Mantén los comandos rápidos disponibles en el menú de Telegram
|
# Mantén los comandos rápidos disponibles en el menú de Telegram
|
||||||
@@ -45,6 +88,7 @@ async def post_init(application: Application):
|
|||||||
BotCommand("welcome", "Registro de nuevas empleadas"),
|
BotCommand("welcome", "Registro de nuevas empleadas"),
|
||||||
BotCommand("vacaciones", "Solicitar vacaciones"),
|
BotCommand("vacaciones", "Solicitar vacaciones"),
|
||||||
BotCommand("permiso", "Solicitar permiso por horas"),
|
BotCommand("permiso", "Solicitar permiso por horas"),
|
||||||
|
BotCommand("links", "Links útiles"),
|
||||||
BotCommand("cancelar", "Cancelar flujo actual"),
|
BotCommand("cancelar", "Cancelar flujo actual"),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -69,6 +113,7 @@ def main():
|
|||||||
app.add_handler(onboarding_handler)
|
app.add_handler(onboarding_handler)
|
||||||
app.add_handler(vacaciones_handler)
|
app.add_handler(vacaciones_handler)
|
||||||
app.add_handler(permiso_handler)
|
app.add_handler(permiso_handler)
|
||||||
|
app.add_handler(CommandHandler("links", links_menu))
|
||||||
# app.add_handler(finder_handler)
|
# app.add_handler(finder_handler)
|
||||||
|
|
||||||
print("🧠 Vanessa Bot Brain iniciada y lista para trabajar en todos los módulos.")
|
print("🧠 Vanessa Bot Brain iniciada y lista para trabajar en todos los módulos.")
|
||||||
|
|||||||
@@ -1,4 +1,18 @@
|
|||||||
import os
|
import os
|
||||||
|
import importlib
|
||||||
|
import importlib.metadata as importlib_metadata
|
||||||
|
|
||||||
|
# Compatibilidad para entornos donde packages_distributions no existe (p.ej. Python 3.9 con importlib recortado).
|
||||||
|
if not hasattr(importlib_metadata, "packages_distributions"):
|
||||||
|
try:
|
||||||
|
import importlib_metadata as backport_metadata # type: ignore
|
||||||
|
if hasattr(backport_metadata, "packages_distributions"):
|
||||||
|
importlib_metadata.packages_distributions = backport_metadata.packages_distributions # type: ignore[attr-defined]
|
||||||
|
else:
|
||||||
|
importlib_metadata.packages_distributions = lambda: {} # type: ignore[assignment]
|
||||||
|
except Exception:
|
||||||
|
importlib_metadata.packages_distributions = lambda: {} # type: ignore[assignment]
|
||||||
|
|
||||||
import google.generativeai as genai
|
import google.generativeai as genai
|
||||||
|
|
||||||
def classify_reason(text: str) -> str:
|
def classify_reason(text: str) -> str:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from telegram.ext import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from modules.database import log_request
|
from modules.database import log_request
|
||||||
|
from modules.ui import main_actions_keyboard
|
||||||
|
|
||||||
# --- 1. CARGA DE ENTORNO ---
|
# --- 1. CARGA DE ENTORNO ---
|
||||||
load_dotenv() # Carga las variables del archivo .env
|
load_dotenv() # Carga las variables del archivo .env
|
||||||
@@ -304,8 +305,14 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|||||||
fecha_ini = "ERROR_FECHA"
|
fecha_ini = "ERROR_FECHA"
|
||||||
# Derivados
|
# Derivados
|
||||||
num_ext_texto = numero_a_texto(r.get(NUM_EXTERIOR, ""), r.get(NUM_INTERIOR, ""))
|
num_ext_texto = numero_a_texto(r.get(NUM_EXTERIOR, ""), r.get(NUM_INTERIOR, ""))
|
||||||
curp_base = (r.get(CURP) or "").upper()
|
# El número de empleado debe ser solo la fecha de inicio en formato AAMMDD.
|
||||||
n_empleado = f"{(curp_base + 'XXXX')[:4]}{fecha_ini.replace('-', '')}"
|
try:
|
||||||
|
fecha_inicio_dt = datetime.strptime(fecha_ini, "%Y-%m-%d")
|
||||||
|
n_empleado = fecha_inicio_dt.strftime("%y%m%d")
|
||||||
|
except Exception:
|
||||||
|
# Fallback defensivo para no romper el flujo si viene un formato raro.
|
||||||
|
fecha_compacta = fecha_ini.replace("-", "")
|
||||||
|
n_empleado = fecha_compacta[-6:] if len(fecha_compacta) >= 6 else fecha_compacta or "N/A"
|
||||||
|
|
||||||
# PAYLOAD ESTRUCTURADO PARA N8N
|
# PAYLOAD ESTRUCTURADO PARA N8N
|
||||||
payload = {
|
payload = {
|
||||||
@@ -378,18 +385,22 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
"✅ *¡Registro Exitoso!*\n\n"
|
"✅ *¡Registro Exitoso!*\n\n"
|
||||||
"Bienvenida a la familia Soul/Vanity. Tu contrato se está generando y te avisaremos pronto.\n"
|
"Bienvenida a la familia Soul/Vanity. Tu contrato se está generando y te avisaremos pronto.\n"
|
||||||
"¡Nos vemos el primer día! ✨"
|
"¡Nos vemos el primer día! ✨",
|
||||||
|
reply_markup=main_actions_keyboard()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await update.message.reply_text("⚠️ Se guardaron tus datos pero hubo un error de conexión. RH lo revisará manualmente.")
|
await update.message.reply_text(
|
||||||
|
"⚠️ Se guardaron tus datos pero hubo un error de conexión. RH lo revisará manualmente.",
|
||||||
|
reply_markup=main_actions_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
context.user_data.clear()
|
context.user_data.clear()
|
||||||
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:
|
||||||
await update.message.reply_text(
|
await update.message.reply_text(
|
||||||
"Proceso cancelado. ⏸️\nCuando quieras retomar, escribe /contrato.",
|
"Proceso cancelado. ⏸️\nPuedes retomarlo con /welcome o ir al menú con /start.",
|
||||||
reply_markup=ReplyKeyboardRemove()
|
reply_markup=main_actions_keyboard()
|
||||||
)
|
)
|
||||||
context.user_data.clear()
|
context.user_data.clear()
|
||||||
return ConversationHandler.END
|
return ConversationHandler.END
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from datetime import datetime, date
|
|||||||
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
|
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
|
||||||
from telegram.ext import CommandHandler, ContextTypes, ConversationHandler, 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.ui import main_actions_keyboard
|
||||||
from modules.ai import classify_reason
|
from modules.ai import classify_reason
|
||||||
|
|
||||||
# Helpers de webhooks
|
# Helpers de webhooks
|
||||||
@@ -342,18 +343,30 @@ async def recibir_motivo_fin(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
f"- Estatus inicial: {payload.get('status_inicial', 'N/A')}"
|
f"- Estatus inicial: {payload.get('status_inicial', 'N/A')}"
|
||||||
)
|
)
|
||||||
if enviados > 0:
|
if enviados > 0:
|
||||||
await update.message.reply_text(f"✅ Solicitud de *{tipo_solicitud_texto}* enviada a tu Manager.\n\n{resumen}")
|
await update.message.reply_text(
|
||||||
|
f"✅ Solicitud de *{tipo_solicitud_texto}* enviada a tu Manager.\n\n{resumen}",
|
||||||
|
reply_markup=main_actions_keyboard()
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await update.message.reply_text(f"⚠️ No hay webhook configurado o falló el envío. RH lo revisará.\n\n{resumen}")
|
await update.message.reply_text(
|
||||||
|
f"⚠️ No hay webhook configurado o falló el envío. RH lo revisará.\n\n{resumen}",
|
||||||
|
reply_markup=main_actions_keyboard()
|
||||||
|
)
|
||||||
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.",
|
||||||
|
reply_markup=main_actions_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
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:
|
||||||
await update.message.reply_text("Solicitud cancelada.")
|
await update.message.reply_text(
|
||||||
|
"Solicitud cancelada. ⏸️\nPuedes volver a iniciar con /vacaciones o /permiso, o ir al menú con /start.",
|
||||||
|
reply_markup=main_actions_keyboard(),
|
||||||
|
)
|
||||||
return ConversationHandler.END
|
return ConversationHandler.END
|
||||||
|
|
||||||
# Handlers separados pero comparten lógica
|
# Handlers separados pero comparten lógica
|
||||||
|
|||||||
13
modules/ui.py
Normal file
13
modules/ui.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from telegram import ReplyKeyboardMarkup
|
||||||
|
|
||||||
|
|
||||||
|
def main_actions_keyboard() -> ReplyKeyboardMarkup:
|
||||||
|
"""Teclado inferior con comandos directos (un toque lanza el flujo)."""
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
|
[
|
||||||
|
["/welcome"],
|
||||||
|
["/vacaciones", "/permiso"],
|
||||||
|
["/links", "/start"],
|
||||||
|
],
|
||||||
|
resize_keyboard=True,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user