feat: agregar Dockerfile y configuración Docker
This commit is contained in:
parent
e774dec930
commit
abc369bd04
8 changed files with 238 additions and 32 deletions
13
.env
13
.env
|
|
@ -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
|
||||
|
|
|
|||
25
.env.example
25
.env.example
|
|
@ -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
83
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
5
dist/worker/worker.service.d.ts
vendored
5
dist/worker/worker.service.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
63
dist/worker/worker.service.js
vendored
63
dist/worker/worker.service.js
vendored
|
|
@ -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;
|
||||
|
|
|
|||
2
dist/worker/worker.service.js.map
vendored
2
dist/worker/worker.service.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -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
Loading…
Add table
Reference in a new issue