Merge pull request #1 from marcogll/feat/docker-mysql-logging-13516104967243096474

feat: Dockerize application and add MySQL logging
This commit is contained in:
Marco Gallegos
2025-12-13 21:30:41 -06:00
committed by GitHub
11 changed files with 213 additions and 107 deletions

6
.env
View File

@@ -3,4 +3,8 @@ TELEGRAM_TOKEN=TU_TOKEN_NUEVO_AQUI
# Webhooks de n8n (puedes agregar más aquí en el futuro) # Webhooks de n8n (puedes agregar más aquí en el futuro)
WEBHOOK_CONTRATO=https://flows.soul23.cloud/webhook/DuXh9Oi7SCAMf9 WEBHOOK_CONTRATO=https://flows.soul23.cloud/webhook/DuXh9Oi7SCAMf9
# WEBHOOK_VACACIONES=https://... (futuro) WEBHOOK_PRINT=
WEBHOOK_VACACIONES=
# --- DATABASE ---
DATABASE_URL=mysql+mysqlconnector://user:password@db:3306/vanessa_logs

93
.gitignore vendored
View File

@@ -1,61 +1,46 @@
# ========================= # Environments
# Secrets & Environment
# =========================
.env .env
.env.*
*.env
# =========================
# Python
# =========================
__pycache__/
*.py[cod]
*.pyo
*.pyd
# Virtual environments
venv/ venv/
.venv/
env/
ENV/
# =========================
# Logs & Runtime
# =========================
*.log
logs/
*.sqlite
*.db
# =========================
# OS & Editors
# =========================
.DS_Store
Thumbs.db
# VS Code
.vscode/
*.code-workspace
# JetBrains
.idea/ .idea/
# ========================= # Byte-compiled / optimized / C extensions
# Build / Distribution __pycache__/
# ========================= *.py[cod]
*$py.class
# Distribution / packaging
.Python
build/ build/
develop-eggs/
dist/ dist/
*.egg-info/
# =========================
# Systemd / Deploy Artifacts
# =========================
*.service
*.pid
# =========================
# Temp / Downloads
# =========================
tmp/
temp/
downloads/ downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
*.log
.pytest_cache/
.hypothesis/

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
# Usar una imagen base de Python
FROM python:3.9-slim
# Establecer el directorio de trabajo
WORKDIR /app
# Copiar los archivos de requisitos e instalar dependencias
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copiar el resto del código de la aplicación
COPY . .
# Comando para ejecutar la aplicación
CMD ["python", "main.py"]

View File

@@ -27,10 +27,13 @@ vanity_bot/
├── .env # Variables sensibles (tokens, URLs) ├── .env # Variables sensibles (tokens, URLs)
├── main.py # Cerebro principal del bot ├── main.py # Cerebro principal del bot
├── requirements.txt # Dependencias ├── requirements.txt # Dependencias
├── Dockerfile # Definición del contenedor del bot
├── docker-compose.yml # Orquestación de servicios (bot + db)
├── README.md # Este documento ├── README.md # Este documento
└── modules/ # Habilidades del bot └── modules/ # Habilidades del bot
├── __init__.py ├── __init__.py
├── database.py # Módulo de conexión a la base de datos
├── onboarding.py # Flujo /welcome (onboarding RH) ├── onboarding.py # Flujo /welcome (onboarding RH)
├── printer.py # Flujo /print (impresión) ├── printer.py # Flujo /print (impresión)
└── rh_requests.py # /vacaciones y /permiso └── rh_requests.py # /vacaciones y /permiso
@@ -50,13 +53,40 @@ TELEGRAM_TOKEN=TU_TOKEN_AQUI
WEBHOOK_ONBOARDING=https://flows.soul23.cloud/webhook/contrato WEBHOOK_ONBOARDING=https://flows.soul23.cloud/webhook/contrato
WEBHOOK_PRINT=https://flows.soul23.cloud/webhook/impresion WEBHOOK_PRINT=https://flows.soul23.cloud/webhook/impresion
WEBHOOK_VACACIONES=https://flows.soul23.cloud/webhook/vacaciones WEBHOOK_VACACIONES=https://flows.soul23.cloud/webhook/vacaciones
# --- DATABASE ---
# Esta URL es para la conexión interna de Docker, no la modifiques si usas Docker Compose.
DATABASE_URL=mysql+mysqlconnector://user:password@db:3306/vanessa_logs
``` ```
Nunca subas este archivo al repositorio. Nunca subas este archivo al repositorio.
--- ---
## 📦 Instalación ## 🐳 Ejecución con Docker (Recomendado)
El proyecto está dockerizado para facilitar su despliegue.
### 1. Pre-requisitos
- Docker
- Docker Compose
### 2. Levantar los servicios
Con el archivo `.env` ya configurado, simplemente ejecuta:
```bash
docker-compose up --build
```
Este comando construirá la imagen del bot, descargará la imagen de MySQL, creará los volúmenes y redes, y lanzará ambos servicios. El bot se conectará automáticamente a la base de datos para registrar los logs.
### 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
```
---
## 📦 Instalación Manual
Se recomienda usar un entorno virtual. Se recomienda usar un entorno virtual.
@@ -79,82 +109,37 @@ Si el token es válido, verás:
``` ```
🧠 Vanessa Brain iniciada y escuchando... 🧠 Vanessa Brain iniciada y escuchando...
``` ```
**Nota**: Para que la ejecución manual funcione, necesitarás tener una base de datos MySQL corriendo localmente y accesible en la URL especificada en `DATABASE_URL` dentro de tu archivo `.env`.
--- ---
## 🧩 Arquitectura Interna ## 🧩 Arquitectura Interna
### main.py (El Cerebro) ### main.py (El Cerebro)
- Inicializa el bot de Telegram - Inicializa el bot de Telegram
- Carga variables de entorno - Carga variables de entorno
- Registra los handlers de cada módulo - Registra los handlers de cada módulo
- Define el menú principal (/start, /help) - Define el menú principal (/start, /help)
Nada de lógica de negocio vive aquí. Solo coordinación. ### 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.
### modules/onboarding.py ### modules/onboarding.py
Flujo conversacional complejo basado en `ConversationHandler`. Flujo conversacional complejo basado en `ConversationHandler`.
- Recolecta información personal, laboral y de emergencia - Recolecta información personal, laboral y de emergencia
- Normaliza datos (RFC, CURP, fechas) - Normaliza datos (RFC, CURP, fechas)
- Usa teclados guiados para reducir errores - Usa teclados guiados para reducir errores
- Envía un payload estructurado a n8n - Envía un payload estructurado a n8n
El diseño es **estado → pregunta → respuesta → siguiente estado**.
---
### modules/printer.py ### modules/printer.py
- Recibe documentos o imágenes desde Telegram - Recibe documentos o imágenes desde Telegram
- Obtiene el enlace temporal de Telegram - Obtiene el enlace temporal de Telegram
- Envía el archivo a una cola de impresión vía webhook - Envía el archivo a una cola de impresión vía webhook
Telegram se usa como interfaz, n8n como backend operativo.
---
### modules/rh_requests.py ### modules/rh_requests.py
- Maneja solicitudes simples de RH: Vacaciones y Permisos por horas.
- Maneja solicitudes simples de RH
- Vacaciones
- Permisos por horas
El bot solo valida y recopila; la lógica de aprobación vive fuera.
---
## ⚙️ Ejecución Automática con systemd (Linux)
Ejemplo de servicio:
```
[Unit]
Description=Vanessa Bot
After=network.target
[Service]
User=vanity
WorkingDirectory=/opt/vanity_bot
EnvironmentFile=/opt/vanity_bot/.env
ExecStart=/opt/vanity_bot/venv/bin/python main.py
Restart=always
[Install]
WantedBy=multi-user.target
```
Luego:
```
sudo systemctl daemon-reload
sudo systemctl enable vanessa
sudo systemctl start vanessa
```
--- ---
@@ -163,6 +148,8 @@ sudo systemctl start vanessa
- Telegram como UI - Telegram como UI
- Python como cerebro - Python como cerebro
- n8n como sistema nervioso - n8n como sistema nervioso
- Docker para despliegue
- MySQL para persistencia de logs
- Datos estructurados, no mensajes sueltos - Datos estructurados, no mensajes sueltos
- Modularidad total: cada habilidad se enchufa o se quita - Modularidad total: cada habilidad se enchufa o se quita

32
docker-compose.yml Normal file
View File

@@ -0,0 +1,32 @@
version: '3.8'
services:
bot:
build: .
container_name: vanessa_bot
restart: always
env_file:
- .env
environment:
- DATABASE_URL=mysql+mysqlconnector://user:password@db:3306/vanessa_logs
depends_on:
- db
volumes:
- .:/app
db:
image: mysql:8.0
container_name: vanessa_db
restart: always
environment:
MYSQL_DATABASE: vanessa_logs
MYSQL_USER: user
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: rootpassword
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
volumes:
mysql_data:

View File

@@ -9,6 +9,7 @@ from telegram.ext import Application, Defaults, CommandHandler, ContextTypes
from modules.onboarding import onboarding_handler from modules.onboarding import onboarding_handler
from modules.printer import print_handler from modules.printer import print_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.finder import finder_handler (Si lo creas después) # from modules.finder import finder_handler (Si lo creas después)
load_dotenv() load_dotenv()
@@ -18,6 +19,8 @@ logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s
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
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"
"📝 `/welcome` - Iniciar onboarding/contrato\n" "📝 `/welcome` - Iniciar onboarding/contrato\n"

64
modules/database.py Normal file
View File

@@ -0,0 +1,64 @@
import os
from sqlalchemy import create_engine, Column, Integer, String, DateTime, MetaData, Table
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
import logging
# Configuración de logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# Obtener la URL de la base de datos desde las variables de entorno
DATABASE_URL = os.getenv("DATABASE_URL", "mysql+mysqlconnector://user:password@db:3306/vanessa_logs")
# Crear el motor de la base de datos
engine = create_engine(DATABASE_URL)
metadata = MetaData()
# Base para los modelos declarativos
Base = declarative_base()
# Clase que mapea a la tabla de logs
class RequestLog(Base):
__tablename__ = 'request_logs'
id = Column(Integer, primary_key=True)
telegram_id = Column(String(50))
username = Column(String(100))
command = Column(String(100))
message = Column(String(500))
created_at = Column(DateTime, default=datetime.utcnow)
# Función para inicializar la base de datos
def init_db():
try:
logging.info("Inicializando la base de datos y creando tablas si no existen...")
Base.metadata.create_all(bind=engine)
logging.info("Tablas verificadas/creadas correctamente.")
except Exception as e:
logging.error(f"Error al inicializar la base de datos: {e}")
raise
# Crear una sesión para interactuar con la base de datos
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Función para registrar una solicitud en la base de datos
def log_request(telegram_id, username, command, message):
db_session = SessionLocal()
try:
log_entry = RequestLog(
telegram_id=str(telegram_id),
username=username,
command=command,
message=message
)
db_session.add(log_entry)
db_session.commit()
logging.info(f"Log guardado: {command} de {username}")
except Exception as e:
logging.error(f"Error al guardar el log: {e}")
db_session.rollback()
finally:
db_session.close()
# Inicializar la base de datos al arrancar el módulo
init_db()

View File

@@ -17,6 +17,8 @@ from telegram.ext import (
Defaults, Defaults,
) )
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")
@@ -115,6 +117,7 @@ TECLADO_RELACION_EMERGENCIA = ReplyKeyboardMarkup(
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
user = update.effective_user user = update.effective_user
context.user_data.clear() context.user_data.clear()
log_request(user.id, user.username, "welcome", update.message.text)
context.user_data["metadata"] = { context.user_data["metadata"] = {
"telegram_id": user.id, "telegram_id": user.id,

View File

@@ -2,11 +2,14 @@ import os
import requests import requests
from telegram import Update 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
# Estado # Estado
ESPERANDO_ARCHIVO = 1 ESPERANDO_ARCHIVO = 1
async def start_print(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: async def start_print(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
user = update.effective_user
log_request(user.id, user.username, "print", update.message.text)
await update.message.reply_text("🖨️ **Servicio de Impresión**\n\nPor favor, envíame el archivo (PDF, DOCX o Imagen) que deseas imprimir/enviar.") await update.message.reply_text("🖨️ **Servicio de Impresión**\n\nPor favor, envíame el archivo (PDF, DOCX o Imagen) que deseas imprimir/enviar.")
return ESPERANDO_ARCHIVO return ESPERANDO_ARCHIVO

View File

@@ -2,15 +2,20 @@ import os
import requests import requests
from telegram import Update, ReplyKeyboardMarkup, ReplyKeyboardRemove from telegram import Update, ReplyKeyboardMarkup, ReplyKeyboardRemove
from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters
from modules.database import log_request
TIPO_SOLICITUD, FECHAS, MOTIVO = range(3) TIPO_SOLICITUD, FECHAS, MOTIVO = range(3)
async def start_vacaciones(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: async def start_vacaciones(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
user = update.effective_user
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? (Ej: 10 al 15 de Octubre)")
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
log_request(user.id, user.username, "permiso", update.message.text)
context.user_data['tipo'] = 'Permiso Especial' context.user_data['tipo'] = 'Permiso Especial'
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\n¿Para qué día y horario lo necesitas?")
return FECHAS return FECHAS

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
python-telegram-bot
python-dotenv
requests
SQLAlchemy
mysql-connector-python