feat: agregar Dockerfile y configuración Docker

This commit is contained in:
DillusDev001 2026-09-18 22:50:02 -04:00
parent e774dec930
commit abc369bd04
8 changed files with 238 additions and 32 deletions

13
.env
View file

@ -1,12 +1,19 @@
# API Principal
API_URL=http://localhost:3000/v1
API_URL=http://192.168.1.100:3000/v1
# API_URL=http://localhost:3000/v1
# API KEY para conectar con el API principal
WORKER_KEY=885E2E87-6B8C-4CBC-93D8-D07CC5A13158
# OpenWA
OPENWA_URL=http://localhost:2785
OPENWA_API_KEY=owa_k1_e2a44d3144f5870553ebdb52dfe373d89fd056f7fb90ef17ce14ca3f889f4bcc
# OPENWA_URL=http://localhost:2785
OPENWA_URL=https://whatsapp.alphaxdev.cc
# Local
# OPENWA_API_KEY=owa_k1_e2a44d3144f5870553ebdb52dfe373d89fd056f7fb90ef17ce14ca3f889f4bcc
# SERVER alphaxdev.cc
OPENWA_API_KEY=owa_k1_f3e283de607aabd68462cd449c980ab18c4f1c6286bdeea3f5e64fd86339728a
# Configuración del worker
WORKER_INTERVAL=3000

View file

@ -1,17 +1,22 @@
cd ~/docker/OpenWA
cp .env.example .env
# API Principal
API_URL=http://localhost:3000/v1
API_URL=http://192.168.1.100:3000/v1
# API_URL=http://localhost:3000/v1
# API KEY para conectar con el API principal
WORKER_KEY=uuid-generado-aqui
WORKER_KEY=885E2E87-6B8C-4CBC-93D8-D07CC5A13158
# OpenWA
OPENWA_URL=http://localhost:2785
OPENWA_API_KEY=tu-api-key-de-openwa
# OPENWA_URL=http://localhost:2785
OPENWA_URL=https://whatsapp.alphaxdev.cc
# Local
# OPENWA_API_KEY=owa_k1_e2a44d3144f5870553ebdb52dfe373d89....
# SERVER alphaxdev.cc
OPENWA_API_KEY=owa_k1_f3e283de607aabd68462cd449c980ab18c4f...
# Configuración del worker
WORKER_INTERVAL=30000
WORKER_LIMIT=5
PORT=3003
WORKER_INTERVAL=3000
PORT=3001
cp .env.example .env

83
.gitignore vendored
View file

@ -1 +1,84 @@
# Dependencies
node_modules/
node_modules
.pnp
.pnp.js
# Build output
dist/
build/
.next/
out/
# Testing
coverage/
# Environment
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
logs/
*.log
# OS files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDE
.idea/
.vscode/
*.swp
*.swo
*.sublime-workspace
*.sublime-project
# Data and storage
data/
media/
!src/**/media/
uploads/
.wwebjs_auth/
.wwebjs_cache/
# Database
*.db
*.sqlite
*.sqlite3
# Docker
.docker/
# Misc
*.bak
*.tmp
*.temp
# Git worktrees
.worktrees/
# AI Agents
.playwright-mcp/
.remember/
.agent/
.claude/
.zcode/
.zcode-worktrees/
docs/plans/
docs/superpowers/
CLAUDE.md
_docs/
.superpowers/
# TypeScript incremental build cache (tsc writes this at repo root under the TS6 build config)
*.tsbuildinfo

View file

@ -11,6 +11,9 @@ export declare class WorkerService implements OnModuleInit, OnModuleDestroy {
private readonly openwaUrl;
private readonly openwaApiKey;
private readonly intervalMs;
private sesionesCache;
private sesionesCacheTimestamp;
private readonly SESIONES_CACHE_TTL;
constructor(config: ConfigService, http: HttpService);
onModuleInit(): void;
onModuleDestroy(): void;
@ -18,6 +21,8 @@ export declare class WorkerService implements OnModuleInit, OnModuleDestroy {
private procesarCiclo;
private procesar;
private obtenerPendiente;
private obtenerSesionesDisponibles;
private obtenerSesionRandom;
private procesarMensaje;
private calcularDelayEscritura;
private calcularDelayEntreMensajes;

View file

@ -22,6 +22,9 @@ let WorkerService = WorkerService_1 = class WorkerService {
openwaUrl;
openwaApiKey;
intervalMs;
sesionesCache = [];
sesionesCacheTimestamp = 0;
SESIONES_CACHE_TTL = 5 * 60 * 1000;
constructor(config, http) {
this.config = config;
this.http = http;
@ -75,13 +78,46 @@ let WorkerService = WorkerService_1 = class WorkerService {
return null;
}
}
async obtenerSesionesDisponibles() {
const ahora = Date.now();
if (this.sesionesCache.length > 0 && ahora - this.sesionesCacheTimestamp < this.SESIONES_CACHE_TTL) {
return this.sesionesCache;
}
try {
const response = await firstValueFrom(this.http.get(`${this.openwaUrl}/api/sessions`, {
headers: { 'X-API-Key': this.openwaApiKey },
}));
const todas = response.data;
this.sesionesCache = todas
.filter((s) => s.status === 'ready')
.map((s) => ({ id: s.id, name: s.name, phone: s.phone }));
this.sesionesCacheTimestamp = ahora;
this.logger.log(`[${this.timestamp()}] 🔄 Sesiones OpenWA actualizadas: ${this.sesionesCache.length} disponibles`);
}
catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`[${this.timestamp()}] Error obteniendo sesiones OpenWA: ${message}`);
}
return this.sesionesCache;
}
async obtenerSesionRandom() {
const sesiones = await this.obtenerSesionesDisponibles();
if (sesiones.length === 0) {
this.logger.warn(`[${this.timestamp()}] ⚠️ No hay sesiones OpenWA disponibles`);
return null;
}
const elegida = sesiones[Math.floor(Math.random() * sesiones.length)];
this.logger.log(`[${this.timestamp()}] 🎲 Sesión seleccionada: ${elegida.name} (${elegida.phone})`);
return elegida.id;
}
async procesarMensaje(mensaje) {
const { idMensajeWhatsApp, telefonoDestino, sessionId, tipo, mensaje1, mensaje2, mensaje3, urlQR } = mensaje;
const { idMensajeWhatsApp, telefonoDestino, tipo, mensaje1, mensaje2, mensaje3, urlQR } = mensaje;
const inicio = Date.now();
this.logger.log(`[${this.timestamp()}] ▶ Procesando mensaje ${idMensajeWhatsApp}${telefonoDestino} (${tipo})`);
try {
const sessionId = await this.obtenerSesionRandom();
if (!sessionId) {
await this.actualizarEstado(idMensajeWhatsApp, 'ERROR', 'No sessionId configurado');
await this.actualizarEstado(idMensajeWhatsApp, 'ERROR', 'No hay sesiones OpenWA disponibles');
return;
}
const chatId = `${telefonoDestino}@c.us`;
@ -89,8 +125,21 @@ let WorkerService = WorkerService_1 = class WorkerService {
if (mensajes.length > 0) {
const texto1 = mensajes[0];
if (tipo === 'RECEPCION_PAQUETE' && urlQR) {
await this.enviarImagen(sessionId, chatId, urlQR, texto1);
this.logger.log(`[${this.timestamp()}] 📷✉️ Mensaje 1 (QR + imagen) enviado INMEDIATO`);
try {
await this.enviarImagen(sessionId, chatId, urlQR, texto1);
this.logger.log(`[${this.timestamp()}] 📷✉️ Mensaje 1 (QR + imagen) enviado INMEDIATO`);
}
catch {
this.logger.warn(`[${this.timestamp()}] ⚠️ Error enviando imagen, usando fallback con link`);
const fullUrl = urlQR.startsWith('http') ? urlQR : `${this.apiUrl.replace('/v1', '')}${urlQR}`;
const textoConQR = `${texto1}\n\n🔗 *Ver QR:* ${fullUrl}`;
await this.enviarTexto(sessionId, chatId, textoConQR);
this.logger.log(`[${this.timestamp()}] ✉️ Mensaje 1 (QR como link) enviado INMEDIATO`);
}
if (mensajes.length > 1) {
await this.actualizarEstado(idMensajeWhatsApp, 'ENVIANDO');
this.logger.log(`[${this.timestamp()}] 🔄 Estado cambiado a ENVIANDO`);
}
}
else {
await this.enviarTexto(sessionId, chatId, texto1);
@ -118,11 +167,11 @@ let WorkerService = WorkerService_1 = class WorkerService {
await this.delay(delayEntre);
}
}
this.logger.log(`[${this.timestamp()}] ⏳ Esperando delay1: ${delay1Seg}s antes de continuar...`);
await this.delay(delay1);
await this.actualizarEstado(idMensajeWhatsApp, 'ENVIADO');
const totalSeg = ((Date.now() - inicio) / 1000).toFixed(1);
this.logger.log(`[${this.timestamp()}] ✅ Mensaje ${idMensajeWhatsApp} ENVIADO (${mensajes.length} mensajes) → tiempo total: ${totalSeg}s`);
this.logger.log(`[${this.timestamp()}] ⏳ Esperando delay1: ${delay1Seg}s antes de continuar...`);
await this.delay(delay1);
}
catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
@ -134,7 +183,7 @@ let WorkerService = WorkerService_1 = class WorkerService {
}
}
calcularDelayEscritura(caracteres) {
const caracteresPorMinuto = 200;
const caracteresPorMinuto = 350;
const base = (caracteres / caracteresPorMinuto) * 60 * 1000;
const random = 1 + (Math.random() * 0.6 - 0.3);
const reaccion = 500 + Math.random() * 1000;

File diff suppressed because one or more lines are too long

View file

@ -26,6 +26,11 @@ export class WorkerService implements OnModuleInit, OnModuleDestroy {
private readonly openwaApiKey: string;
private readonly intervalMs: number;
// Cache de sesiones OpenWA
private sesionesCache: { id: string; name: string; phone: string }[] = [];
private sesionesCacheTimestamp = 0;
private readonly SESIONES_CACHE_TTL = 5 * 60 * 1000; // 5 minutos
constructor(
private config: ConfigService,
private http: HttpService,
@ -92,15 +97,53 @@ export class WorkerService implements OnModuleInit, OnModuleDestroy {
}
}
private async obtenerSesionesDisponibles(): Promise<{ id: string; name: string; phone: string }[]> {
const ahora = Date.now();
if (this.sesionesCache.length > 0 && ahora - this.sesionesCacheTimestamp < this.SESIONES_CACHE_TTL) {
return this.sesionesCache;
}
try {
const response = await firstValueFrom(
this.http.get(`${this.openwaUrl}/api/sessions`, {
headers: { 'X-API-Key': this.openwaApiKey },
}),
);
const todas = response.data;
this.sesionesCache = todas
.filter((s: any) => s.status === 'ready')
.map((s: any) => ({ id: s.id, name: s.name, phone: s.phone }));
this.sesionesCacheTimestamp = ahora;
this.logger.log(`[${this.timestamp()}] 🔄 Sesiones OpenWA actualizadas: ${this.sesionesCache.length} disponibles`);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`[${this.timestamp()}] Error obteniendo sesiones OpenWA: ${message}`);
}
return this.sesionesCache;
}
private async obtenerSesionRandom(): Promise<string | null> {
const sesiones = await this.obtenerSesionesDisponibles();
if (sesiones.length === 0) {
this.logger.warn(`[${this.timestamp()}] ⚠️ No hay sesiones OpenWA disponibles`);
return null;
}
const elegida = sesiones[Math.floor(Math.random() * sesiones.length)];
this.logger.log(`[${this.timestamp()}] 🎲 Sesión seleccionada: ${elegida.name} (${elegida.phone})`);
return elegida.id;
}
private async procesarMensaje(mensaje: MensajePendiente) {
const { idMensajeWhatsApp, telefonoDestino, sessionId, tipo, mensaje1, mensaje2, mensaje3, urlQR } = mensaje;
const { idMensajeWhatsApp, telefonoDestino, tipo, mensaje1, mensaje2, mensaje3, urlQR } = mensaje;
const inicio = Date.now();
this.logger.log(`[${this.timestamp()}] ▶ Procesando mensaje ${idMensajeWhatsApp}${telefonoDestino} (${tipo})`);
try {
const sessionId = await this.obtenerSesionRandom();
if (!sessionId) {
await this.actualizarEstado(idMensajeWhatsApp, 'ERROR', 'No sessionId configurado');
await this.actualizarEstado(idMensajeWhatsApp, 'ERROR', 'No hay sesiones OpenWA disponibles');
return;
}
@ -111,8 +154,22 @@ export class WorkerService implements OnModuleInit, OnModuleDestroy {
if (mensajes.length > 0) {
const texto1 = mensajes[0];
if (tipo === 'RECEPCION_PAQUETE' && urlQR) {
await this.enviarImagen(sessionId, chatId, urlQR, texto1);
this.logger.log(`[${this.timestamp()}] 📷✉️ Mensaje 1 (QR + imagen) enviado INMEDIATO`);
// Intentar enviar imagen; si falla, enviar QR como link
try {
await this.enviarImagen(sessionId, chatId, urlQR, texto1);
this.logger.log(`[${this.timestamp()}] 📷✉️ Mensaje 1 (QR + imagen) enviado INMEDIATO`);
} catch {
this.logger.warn(`[${this.timestamp()}] ⚠️ Error enviando imagen, usando fallback con link`);
const fullUrl = urlQR.startsWith('http') ? urlQR : `${this.apiUrl.replace('/v1', '')}${urlQR}`;
const textoConQR = `${texto1}\n\n🔗 *Ver QR:* ${fullUrl}`;
await this.enviarTexto(sessionId, chatId, textoConQR);
this.logger.log(`[${this.timestamp()}] ✉️ Mensaje 1 (QR como link) enviado INMEDIATO`);
}
// Cambiar estado a ENVIANDO después del primer mensaje (RECEPCION_PAQUETE tiene 3 mensajes)
if (mensajes.length > 1) {
await this.actualizarEstado(idMensajeWhatsApp, 'ENVIANDO');
this.logger.log(`[${this.timestamp()}] 🔄 Estado cambiado a ENVIANDO`);
}
} else {
await this.enviarTexto(sessionId, chatId, texto1);
this.logger.log(`[${this.timestamp()}] ✉️ Mensaje 1 enviado INMEDIATO`);
@ -152,15 +209,15 @@ export class WorkerService implements OnModuleInit, OnModuleDestroy {
}
}
// ── PASO 7: Esperar delay1 (el que se calculó en paso 2) ──
this.logger.log(`[${this.timestamp()}] ⏳ Esperando delay1: ${delay1Seg}s antes de continuar...`);
await this.delay(delay1);
// Marcar como enviado
// Marcar como enviado ANTES del delay para no perder el estado si el worker se cae
await this.actualizarEstado(idMensajeWhatsApp, 'ENVIADO');
const totalSeg = ((Date.now() - inicio) / 1000).toFixed(1);
this.logger.log(`[${this.timestamp()}] ✅ Mensaje ${idMensajeWhatsApp} ENVIADO (${mensajes.length} mensajes) → tiempo total: ${totalSeg}s`);
// ── PASO 7: Esperar delay1 (el que se calculó en paso 2) ──
this.logger.log(`[${this.timestamp()}] ⏳ Esperando delay1: ${delay1Seg}s antes de continuar...`);
await this.delay(delay1);
} catch (error: any) {
const message = error instanceof Error ? error.message : 'Unknown error';
const responseMessage = error?.response?.data?.message || error?.response?.data?.error || '';
@ -172,7 +229,7 @@ export class WorkerService implements OnModuleInit, OnModuleDestroy {
}
private calcularDelayEscritura(caracteres: number): number {
const caracteresPorMinuto = 200;
const caracteresPorMinuto = 350;
const base = (caracteres / caracteresPorMinuto) * 60 * 1000;
const random = 1 + (Math.random() * 0.6 - 0.3);
const reaccion = 500 + Math.random() * 1000;

File diff suppressed because one or more lines are too long