mirror of
https://github.com/marcogll/telegram_new_socias.git
synced 2026-01-13 13:15:16 +00:00
feat: Implement direct MySQL database integration for onboarding and duplicate checks, add Gemini AI support, and update webhook and email configurations.
This commit is contained in:
59
.env.example
59
.env.example
@@ -1,49 +1,40 @@
|
||||
# Configuración de Telegram
|
||||
TELEGRAM_TOKEN=TU_TOKEN_NUEVO_AQUI
|
||||
TELEGRAM_ADMIN_CHAT_ID=TELEGRAM_ADMIN_CHAT_ID
|
||||
OPENAI_API_KEY=SK......
|
||||
GOOGLE_API_KEY=AIzaSyBqH5...
|
||||
OPENAI_API_KEY=sk-proj-xxxx
|
||||
GOOGLE_API_KEY=AIzaSyBqH5... # Usado para Gemini AI en modules/ai.py
|
||||
|
||||
# URL de la hoja de cálculo de Google para verificar duplicados
|
||||
GOOGLE_SHEET_URL=https://docs.google.com/spreadsheets/d/1iVHnNoAF4sVVhb2kcclthznYFUKetmhsM6b2ZUCXd-0/edit?gid=370216950#gid=370216950
|
||||
|
||||
# Opcional: Credenciales de Google como variables de entorno
|
||||
# Si estas variables están definidas, se usarán en lugar del archivo JSON.
|
||||
# Asegúrate de escapar correctamente el valor de GSA_PRIVATE_KEY (ej. reemplazando saltos de línea con \n)
|
||||
GSA_TYPE=service_account
|
||||
GSA_PROJECT_ID=
|
||||
GSA_PRIVATE_KEY_ID=
|
||||
GSA_PRIVATE_KEY=
|
||||
GSA_CLIENT_EMAIL=
|
||||
GSA_CLIENT_ID=
|
||||
GSA_AUTH_URI=
|
||||
GSA_TOKEN_URI=
|
||||
GSA_AUTH_PROVIDER_X509_CERT_URL=
|
||||
GSA_CLIENT_X509_CERT_URL=
|
||||
|
||||
|
||||
# Webhooks de n8n (puedes agregar más aquí en el futuro)
|
||||
# Usa WEBHOOK_ONBOARDING (o el alias WEBHOOK_CONTRATO si ya lo tienes así)
|
||||
# ===============================
|
||||
# WEBHOOKS
|
||||
# ===============================
|
||||
WEBHOOK_ONBOARDING=url
|
||||
# WEBHOOK_CONTRATO=url
|
||||
WEBHOOK_PRINT=url
|
||||
WEBHOOK_VACACIONES=url
|
||||
WEBHOOK_PERMISOS=url
|
||||
WEBHOOK_PRINTS=url
|
||||
WEBHOOK_SCHEDULE=url
|
||||
|
||||
# --- DATABASE ---
|
||||
# Usado por el servicio de la base de datos en docker-compose.yml
|
||||
MYSQL_DATABASE_USERS_ALMA=USERS_ALMA
|
||||
MYSQL_DATABASE_VANITY_HR=vanity_hr
|
||||
MYSQL_DATABASE_VANITY_ATTENDANCE=vanity_attendance
|
||||
# ===============================
|
||||
# DATABASE SETUP
|
||||
# ===============================
|
||||
MYSQL_HOST=db
|
||||
MYSQL_USER=user
|
||||
MYSQL_PASSWORD=password
|
||||
MYSQL_ROOT_PASSWORD=rootpassword
|
||||
|
||||
# --- SMTP ---
|
||||
# Usado por el módulo de impresión para enviar correos
|
||||
# Database Names
|
||||
MYSQL_DATABASE_USERS_ALMA=USERS_ALMA
|
||||
MYSQL_DATABASE_VANITY_HR=vanity_hr
|
||||
MYSQL_DATABASE_VANITY_ATTENDANCE=vanity_attendance
|
||||
|
||||
# ===============================
|
||||
# EMAIL SETUP
|
||||
# ===============================
|
||||
SMTP_SERVER=smtp.hostinger.com
|
||||
SMTP_PORT=465
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your_email@example.com
|
||||
SMTP_PASSWORD=your_password
|
||||
SMTP_RECIPIENT=your_email@example.com # También se acepta PRINTER_EMAIL como alias
|
||||
GOOGLE_CREDENTIALS_FILE=google_credentials.json
|
||||
IMAP_SERVER=imap.hostinger.com
|
||||
IMAP_PORT=993
|
||||
IMAP_USER=your_email@example.com
|
||||
IMAP_PASSWORD=your_password
|
||||
PRINTER_EMAIL=your_printer_email@example.com
|
||||
|
||||
125
Readme.md
125
Readme.md
@@ -1,6 +1,6 @@
|
||||
# 🤖 Vanessa Bot – Asistente de RH para Vanity
|
||||
|
||||
Vanessa es un bot de Telegram escrito en Python que automatiza procesos internos de Recursos Humanos en Vanity. Su objetivo es eliminar fricción operativa: onboarding y solicitudes de RH, todo orquestado desde Telegram y conectado a flujos de n8n o servicios de correo.
|
||||
Vanessa es un bot de Telegram escrito en Python que automatiza procesos internos de Recursos Humanos en Vanity. Su objetivo es eliminar fricción operativa: onboarding y solicitudes de RH, todo orquestado desde Telegram y conectado a flujos de n8n, servicios de correo y bases de datos MySQL.
|
||||
|
||||
Este repositorio está pensado como **proyecto Python profesional**, modular y listo para correr 24/7 en producción.
|
||||
|
||||
@@ -10,11 +10,11 @@ Este repositorio está pensado como **proyecto Python profesional**, modular y l
|
||||
|
||||
Vanessa no es un chatbot genérico: es una interfaz conversacional para procesos reales de negocio.
|
||||
|
||||
- Onboarding completo de nuevas socias (`/welcome`)
|
||||
- Solicitud de vacaciones (`/vacaciones`)
|
||||
- Solicitud de permisos por horas (`/permiso`)
|
||||
- **Onboarding completo de nuevas socias (`/welcome`)**: Recolecta datos, valida que no existan duplicados en la DB, registra a la usuaria en `USERS_ALMA` y envía los datos a n8n.
|
||||
- **Solicitud de vacaciones (`/vacaciones`)**: Flujo dinámico para gestionar días de descanso.
|
||||
- **Solicitud de permisos por horas (`/permiso`)**: Incluye clasificación de motivos mediante IA (Gemini).
|
||||
|
||||
Cada flujo es un módulo independiente, y los datos se envían a **webhooks de n8n**.
|
||||
Cada flujo es un módulo independiente que interactúa con la base de datos y flujos de **n8n**.
|
||||
|
||||
---
|
||||
|
||||
@@ -31,124 +31,91 @@ vanity_bot/
|
||||
├── docker-compose.yml # Orquestación de servicios (bot + db)
|
||||
├── README.md # Este documento
|
||||
│
|
||||
├── models/ # Modelos de base de datos (SQLAlchemy)
|
||||
│ ├── users_alma_models.py
|
||||
│ ├── vanity_hr_models.py
|
||||
│ └── vanity_attendance_models.py
|
||||
│
|
||||
└── modules/ # Habilidades del bot
|
||||
├── __init__.py
|
||||
├── database.py # Módulo de conexión a la base de datos
|
||||
├── onboarding.py # Flujo /welcome (onboarding RH)
|
||||
└── rh_requests.py # /vacaciones y /permiso
|
||||
├── ai.py # Clasificación de motivos con Gemini
|
||||
├── database.py # Conexión a DB y lógica de negocio (registro/verificación)
|
||||
├── logger.py # Registro de auditoría
|
||||
├── onboarding.py # Flujo /welcome
|
||||
├── rh_requests.py # /vacaciones y /permiso
|
||||
└── ui.py # Teclados y componentes de interfaz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Configuración (.env)
|
||||
|
||||
Copia el archivo `.env.example` a `.env` y rellena los valores correspondientes. Este archivo es ignorado por Git para proteger tus credenciales.
|
||||
Vanessa utiliza múltiples bases de datos y webhooks. Asegúrate de configurar correctamente los nombres de las bases de datos.
|
||||
|
||||
```
|
||||
```ini
|
||||
# --- TELEGRAM ---
|
||||
TELEGRAM_TOKEN=TU_TOKEN_AQUI
|
||||
|
||||
# --- AI (Gemini) ---
|
||||
GOOGLE_API_KEY=AIzaSy...
|
||||
|
||||
# --- WEBHOOKS N8N ---
|
||||
WEBHOOK_ONBOARDING=https://... # Alias aceptado: WEBHOOK_CONTRATO
|
||||
WEBHOOK_ONBOARDING=https://...
|
||||
WEBHOOK_VACACIONES=https://...
|
||||
WEBHOOK_PERMISOS=https://...
|
||||
|
||||
# --- DATABASE ---
|
||||
# Usado por el servicio de la base de datos en docker-compose.yml
|
||||
MYSQL_DATABASE=vanessa_logs
|
||||
# --- DATABASE SETUP ---
|
||||
MYSQL_HOST=db
|
||||
MYSQL_USER=user
|
||||
MYSQL_PASSWORD=password
|
||||
MYSQL_ROOT_PASSWORD=rootpassword
|
||||
|
||||
# Nombres de las Bases de Datos
|
||||
MYSQL_DATABASE_USERS_ALMA=USERS_ALMA
|
||||
MYSQL_DATABASE_VANITY_HR=vanity_hr
|
||||
MYSQL_DATABASE_VANITY_ATTENDANCE=vanity_attendance
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Ejecución con Docker (Recomendado)
|
||||
|
||||
El proyecto está dockerizado para facilitar su despliegue.
|
||||
El proyecto está dockerizado para facilitar su despliegue y aislamiento.
|
||||
|
||||
### 1. Pre-requisitos
|
||||
- Docker
|
||||
- Docker Compose
|
||||
- Docker y Docker Compose instalaros.
|
||||
|
||||
### 2. Levantar los servicios
|
||||
Con el archivo `.env` ya configurado, simplemente ejecuta:
|
||||
```bash
|
||||
docker-compose up --build
|
||||
docker-compose up --build -d
|
||||
```
|
||||
Este comando construirá la imagen del bot, descargará la imagen de MySQL, y lanzará ambos servicios. `docker-compose` leerá las variables del archivo `.env` para configurar los contenedores.
|
||||
|
||||
### 3. Detener los servicios
|
||||
Para detener los contenedores, presiona `Ctrl+C` en la terminal donde se están ejecutando, o ejecuta desde otro terminal:
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### 4. Despliegue con imagen pre-construida (Collify)
|
||||
Si Collify solo consume imágenes ya publicadas, usa el archivo `docker-compose.collify.yml` que apunta a una imagen en registro (`DOCKER_IMAGE`).
|
||||
|
||||
1) Construir y publicar la imagen (ejemplo con Buildx y tag con timestamp):
|
||||
```bash
|
||||
export DOCKER_IMAGE=marcogll/vanessa-bot:prod-$(date +%Y%m%d%H%M)
|
||||
docker buildx build --platform linux/amd64 -t $DOCKER_IMAGE . --push
|
||||
```
|
||||
|
||||
2) Desplegar en el servidor (Collify) usando la imagen publicada:
|
||||
```bash
|
||||
export DOCKER_IMAGE=marcogll/vanessa-bot:prod-20240101
|
||||
docker compose -f docker-compose.collify.yml pull
|
||||
docker compose -f docker-compose.collify.yml up -d
|
||||
```
|
||||
`docker-compose.collify.yml` usa `env_file: .env`, así que carga las credenciales igual que en local o configúralas como variables de entorno en la plataforma.
|
||||
Este comando levantará el bot y un contenedor de MySQL (si se usa el compose por defecto). El bot se reconectará automáticamente a la DB si esta tarda en iniciar.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Arquitectura Interna
|
||||
|
||||
### main.py (El Cerebro)
|
||||
- Inicializa el bot de Telegram
|
||||
- Carga variables de entorno
|
||||
- Registra los handlers de cada módulo
|
||||
- Define el menú principal (`/start`, `/help`)
|
||||
- Inicializa el bot de Telegram y carga variables de entorno.
|
||||
- Registra los handlers de cada módulo y define el menú principal y comandos persistentes.
|
||||
|
||||
### modules/database.py
|
||||
- Gestiona la conexión a la base de datos MySQL con SQLAlchemy.
|
||||
- Define el modelo `RequestLog` para la tabla de logs.
|
||||
- Provee la función `log_request` para registrar interacciones.
|
||||
- Centraliza la conexión a las 3 bases de datos (`USERS_ALMA`, `vanity_hr`, `vanity_attendance`).
|
||||
- **Verificación de duplicados**: Ya no usa Google Sheets; ahora verifica el `telegram_id` directamente en la tabla `users`.
|
||||
- **Registro de usuarias**: Función `register_user` para insertar candidatas tras el onboarding.
|
||||
|
||||
### modules/onboarding.py
|
||||
Flujo conversacional complejo que recolecta datos de nuevas empleadas y los envía a un webhook de n8n.
|
||||
Incluye derivadas útiles: `num_ext_texto` (número en letras, con interior) y `numero_empleado` (primeras 4 del CURP + fecha de ingreso).
|
||||
Recolección exhaustiva de datos. Al finalizar:
|
||||
1. Valida y formatea datos (RFC, CURP, fechas).
|
||||
2. Registra a la empleada en la base de datos MySQL.
|
||||
3. Envía el payload completo al webhook de n8n para generación de contratos.
|
||||
|
||||
### modules/rh_requests.py
|
||||
- Maneja solicitudes simples de RH (Vacaciones y Permisos) y las envía a un webhook de n8n.
|
||||
- Vacaciones: pregunta año (actual o siguiente), día/mes de inicio y fin, calcula métricas y aplica semáforo automático.
|
||||
- Permisos: ofrece accesos rápidos (hoy/mañana/pasado) o fecha específica (año actual/siguiente, día/mes), pide horario, clasifica motivo con IA y envía al webhook.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Filosofía del Proyecto
|
||||
|
||||
- **Telegram como UI**: Interfaz conversacional accesible para todos.
|
||||
- **Python como cerebro**: Lógica de negocio y orquestación.
|
||||
- **Docker para despliegue**: Entornos consistentes y portátiles.
|
||||
- **MySQL para persistencia**: Registro auditable de todas las interacciones.
|
||||
- **Modularidad total**: Cada habilidad es un componente independiente.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Estado del Proyecto
|
||||
|
||||
✔ Funcional en producción
|
||||
✔ Modular
|
||||
✔ Escalable
|
||||
✔ Auditable
|
||||
|
||||
Vanessa está viva. Y aprende con cada flujo nuevo.
|
||||
### modules/ai.py & modules/rh_requests.py
|
||||
Integración con **Google Gemini** para clasificar automáticamente los motivos de los permisos (Médico, Trámite, etc.) y envío sincronizado a webhooks de gestión humana.
|
||||
|
||||
---
|
||||
|
||||
## 🗒️ Registro de versiones
|
||||
|
||||
- **1.2 (2025-01-25)** — Onboarding: selector de año 2020–2026; `numero_empleado` incluye prefijo CURP (4 chars) + fecha de ingreso; vacaciones/permiso ajustan fin automático al siguiente año cuando aplica.
|
||||
- **1.3 (2025-12-18)** — **Adiós Google Sheets**: Migración total a base de datos MySQL para verificación de existencia y registro de nuevas socias. Limpieza de `.env` y optimización de arquitectura de modelos.
|
||||
- **1.2 (2025-01-25)** — Onboarding: selector de año 2020–2026; `numero_empleado` dinámico; mejoras en flujos de vacaciones/permiso.
|
||||
- **1.1** — Implementación inicial de webhooks y Docker.
|
||||
|
||||
@@ -5,8 +5,7 @@ from sqlalchemy.orm import sessionmaker
|
||||
from models.users_alma_models import Base as BaseUsersAlma, User
|
||||
from models.vanity_hr_models import Base as BaseVanityHr, DataEmpleadas, Vacaciones, Permisos
|
||||
from models.vanity_attendance_models import Base as BaseVanityAttendance, AsistenciaRegistros, HorarioEmpleadas
|
||||
import gspread
|
||||
from google.oauth2.service_account import Credentials
|
||||
|
||||
|
||||
# --- DATABASE (MySQL) SETUP ---
|
||||
def _build_engine(db_name_env_var):
|
||||
@@ -36,69 +35,50 @@ SessionUsersAlma = sessionmaker(autocommit=False, autoflush=False, bind=engine_u
|
||||
SessionVanityHr = sessionmaker(autocommit=False, autoflush=False, bind=engine_vanity_hr) if engine_vanity_hr else None
|
||||
SessionVanityAttendance = sessionmaker(autocommit=False, autoflush=False, bind=engine_vanity_attendance) if engine_vanity_attendance else None
|
||||
|
||||
# --- GOOGLE SHEETS SETUP ---
|
||||
GSHEET_URL = os.getenv("GOOGLE_SHEET_URL")
|
||||
GOOGLE_CREDENTIALS_FILE = os.getenv("GOOGLE_CREDENTIALS_FILE", "google_credentials.json")
|
||||
SHEET_COLUMN_INDEX = 40 # AN is the 40th column
|
||||
|
||||
def get_gsheet_client():
|
||||
"""Returns an authenticated gspread client or None if it fails."""
|
||||
if not GSHEET_URL:
|
||||
logging.warning("GOOGLE_SHEET_URL is not configured. Duplicate checking is disabled.")
|
||||
return None
|
||||
|
||||
creds = None
|
||||
scopes = ["https://www.googleapis.com/auth/spreadsheets.readonly"]
|
||||
|
||||
gsa_creds_dict = {
|
||||
"type": os.getenv("GSA_TYPE"),
|
||||
"project_id": os.getenv("GSA_PROJECT_ID"),
|
||||
"private_key_id": os.getenv("GSA_PRIVATE_KEY_ID"),
|
||||
"private_key": (os.getenv("GSA_PRIVATE_KEY") or "").replace("\\n", "\n"),
|
||||
"client_email": os.getenv("GSA_CLIENT_EMAIL"),
|
||||
"client_id": os.getenv("GSA_CLIENT_ID"),
|
||||
"auth_uri": os.getenv("GSA_AUTH_URI"),
|
||||
"token_uri": os.getenv("GSA_TOKEN_URI"),
|
||||
"auth_provider_x509_cert_url": os.getenv("GSA_AUTH_PROVIDER_X509_CERT_URL"),
|
||||
"client_x509_cert_url": os.getenv("GSA_CLIENT_X509_CERT_URL"),
|
||||
}
|
||||
|
||||
if all(gsa_creds_dict.values()):
|
||||
try:
|
||||
creds = Credentials.from_service_account_info(gsa_creds_dict, scopes=scopes)
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing Google credentials from environment: {e}")
|
||||
return None
|
||||
elif os.path.exists(GOOGLE_CREDENTIALS_FILE):
|
||||
try:
|
||||
creds = Credentials.from_service_account_file(GOOGLE_CREDENTIALS_FILE, scopes=scopes)
|
||||
except Exception as e:
|
||||
logging.error(f"Error processing credentials file '{GOOGLE_CREDENTIALS_FILE}': {e}")
|
||||
return None
|
||||
else:
|
||||
logging.warning("Google credentials not found (neither environment variables nor file). Duplicate checking is disabled.")
|
||||
return None
|
||||
|
||||
try:
|
||||
return gspread.authorize(creds)
|
||||
except Exception as e:
|
||||
logging.error(f"Error authorizing gspread client: {e}")
|
||||
return None
|
||||
# --- GOOGLE SHEETS SETUP (REMOVED) ---
|
||||
# Duplicate checking is now done via database.
|
||||
|
||||
def chat_id_exists(chat_id: int) -> bool:
|
||||
"""Checks if a Telegram chat_id already exists in the Google Sheet."""
|
||||
client = get_gsheet_client()
|
||||
if not client:
|
||||
"""Checks if a Telegram chat_id already exists in the USERS_ALMA.users table."""
|
||||
if not SessionUsersAlma:
|
||||
logging.warning("SessionUsersAlma not initialized. Cannot check if chat_id exists.")
|
||||
return False
|
||||
|
||||
session = SessionUsersAlma()
|
||||
try:
|
||||
exists = session.query(User).filter(User.telegram_id == str(chat_id)).first() is not None
|
||||
return exists
|
||||
except Exception as e:
|
||||
logging.error(f"Error checking if chat_id exists in DB: {e}")
|
||||
return False
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def register_user(user_data: dict) -> bool:
|
||||
"""Registers a new user in the USERS_ALMA.users table."""
|
||||
if not SessionUsersAlma:
|
||||
logging.warning("SessionUsersAlma not initialized. Cannot register user.")
|
||||
return False
|
||||
|
||||
session = SessionUsersAlma()
|
||||
try:
|
||||
spreadsheet = client.open_by_url(GSHEET_URL)
|
||||
worksheet = spreadsheet.get_worksheet(0)
|
||||
chat_ids_in_sheet = worksheet.col_values(SHEET_COLUMN_INDEX)
|
||||
return str(chat_id) in chat_ids_in_sheet
|
||||
except gspread.exceptions.SpreadsheetNotFound:
|
||||
logging.error("Could not find the spreadsheet at the provided URL.")
|
||||
return False
|
||||
new_user = User(
|
||||
telegram_id=str(user_data.get("chat_id")),
|
||||
username=user_data.get("telegram_user"),
|
||||
first_name=user_data.get("first_name"),
|
||||
last_name=f"{user_data.get('apellido_paterno', '')} {user_data.get('apellido_materno', '')}".strip(),
|
||||
email=user_data.get("email"),
|
||||
cell_phone=user_data.get("celular"),
|
||||
role='user' # Default role
|
||||
)
|
||||
session.add(new_user)
|
||||
session.commit()
|
||||
logging.info(f"User {user_data.get('chat_id')} registered successfully in DB.")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Error reading the spreadsheet: {e}")
|
||||
session.rollback()
|
||||
logging.error(f"Error registering user in DB: {e}")
|
||||
return False
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from telegram.ext import (
|
||||
)
|
||||
|
||||
from modules.logger import log_request
|
||||
from modules.database import chat_id_exists
|
||||
from modules.database import chat_id_exists, register_user
|
||||
from modules.ui import main_actions_keyboard
|
||||
|
||||
# --- 1. CARGA DE ENTORNO ---
|
||||
@@ -404,6 +404,19 @@ async def finalizar(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||
except Exception as e:
|
||||
logging.error(f"Error enviando webhook a {url}: {e}")
|
||||
|
||||
# --- REGISTRO EN BASE DE DATOS ---
|
||||
db_ok = register_user({
|
||||
**meta,
|
||||
**payload["metadata"],
|
||||
**payload["candidato"],
|
||||
**payload["contacto"]
|
||||
})
|
||||
|
||||
if db_ok:
|
||||
logging.info(f"Usuario {meta['chat_id']} registrado en la base de datos.")
|
||||
else:
|
||||
logging.error(f"Fallo al registrar usuario {meta['chat_id']} en la base de datos.")
|
||||
|
||||
if enviado:
|
||||
await update.message.reply_text(
|
||||
"✅ *¡Registro Exitoso!*\n\n"
|
||||
|
||||
@@ -4,6 +4,4 @@ requests
|
||||
SQLAlchemy
|
||||
mysql-connector-python
|
||||
google-generativeai
|
||||
openai
|
||||
gspread
|
||||
google-auth-oauthlib
|
||||
openai
|
||||
Reference in New Issue
Block a user