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)
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 @@
# =========================
# Secrets & Environment
# =========================
# Environments
.env
.env.*
*.env
# =========================
# Python
# =========================
__pycache__/
*.py[cod]
*.pyo
*.pyd
# Virtual environments
venv/
.venv/
env/
ENV/
# =========================
# Logs & Runtime
# =========================
*.log
logs/
*.sqlite
*.db
# =========================
# OS & Editors
# =========================
.DS_Store
Thumbs.db
# VS Code
.vscode/
*.code-workspace
# JetBrains
.idea/
# =========================
# Build / Distribution
# =========================
# Byte-compiled / optimized / C extensions
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
*.egg-info/
# =========================
# Systemd / Deploy Artifacts
# =========================
*.service
*.pid
# =========================
# Temp / Downloads
# =========================
tmp/
temp/
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)
├── main.py # Cerebro principal del bot
├── requirements.txt # Dependencias
├── Dockerfile # Definición del contenedor del bot
├── docker-compose.yml # Orquestación de servicios (bot + db)
├── README.md # Este documento
└── modules/ # Habilidades del bot
├── __init__.py
├── database.py # Módulo de conexión a la base de datos
├── onboarding.py # Flujo /welcome (onboarding RH)
├── printer.py # Flujo /print (impresión)
└── rh_requests.py # /vacaciones y /permiso
@@ -50,13 +53,40 @@ TELEGRAM_TOKEN=TU_TOKEN_AQUI
WEBHOOK_ONBOARDING=https://flows.soul23.cloud/webhook/contrato
WEBHOOK_PRINT=https://flows.soul23.cloud/webhook/impresion
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.
---
## 📦 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.
@@ -79,82 +109,37 @@ Si el token es válido, verás:
```
🧠 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
### 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)
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
Flujo conversacional complejo basado en `ConversationHandler`.
- Recolecta información personal, laboral y de emergencia
- Normaliza datos (RFC, CURP, fechas)
- Usa teclados guiados para reducir errores
- Envía un payload estructurado a n8n
El diseño es **estado → pregunta → respuesta → siguiente estado**.
---
### modules/printer.py
- Recibe documentos o imágenes desde Telegram
- Obtiene el enlace temporal de Telegram
- 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
- 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
```
- Maneja solicitudes simples de RH: Vacaciones y Permisos por horas.
---
@@ -163,6 +148,8 @@ sudo systemctl start vanessa
- Telegram como UI
- Python como cerebro
- n8n como sistema nervioso
- Docker para despliegue
- MySQL para persistencia de logs
- Datos estructurados, no mensajes sueltos
- 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.printer import print_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)
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):
"""Muestra el menú de opciones de Vanessa"""
user = update.effective_user
log_request(user.id, user.username, "start", update.message.text)
texto = (
"👩‍💼 **Hola, soy Vanessa. ¿En qué puedo ayudarte hoy?**\n\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,
)
from modules.database import log_request
# --- 1. CARGA DE ENTORNO ---
load_dotenv() # Carga las variables del archivo .env
TOKEN = os.getenv("TELEGRAM_TOKEN")
@@ -115,6 +117,7 @@ TECLADO_RELACION_EMERGENCIA = ReplyKeyboardMarkup(
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
user = update.effective_user
context.user_data.clear()
log_request(user.id, user.username, "welcome", update.message.text)
context.user_data["metadata"] = {
"telegram_id": user.id,

View File

@@ -2,11 +2,14 @@ import os
import requests
from telegram import Update
from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters
from modules.database import log_request
# Estado
ESPERANDO_ARCHIVO = 1
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.")
return ESPERANDO_ARCHIVO

View File

@@ -2,15 +2,20 @@ import os
import requests
from telegram import Update, ReplyKeyboardMarkup, ReplyKeyboardRemove
from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters
from modules.database import log_request
TIPO_SOLICITUD, FECHAS, MOTIVO = range(3)
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'
await update.message.reply_text("🌴 **Solicitud de Vacaciones**\n\n¿Para qué fechas las necesitas? (Ej: 10 al 15 de Octubre)")
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 Especial'
await update.message.reply_text("⏱️ **Solicitud de Permiso**\n\n¿Para qué día y horario lo necesitas?")
return FECHAS

5
requirements.txt Normal file
View File

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