First Commit

This commit is contained in:
DillusDev001 2026-09-18 12:37:37 -04:00
commit e774dec930
43 changed files with 4682 additions and 0 deletions

15
.dockerignore Normal file
View file

@ -0,0 +1,15 @@
node_modules
dist
.git
.gitignore
.env
.env.*
!.env.example
test
*.md
.vscode
.prettierrc
oxlint.json
vitest.config.ts
vitest.config.e2e.ts
tsconfig.build.tsbuildinfo

13
.env Normal file
View file

@ -0,0 +1,13 @@
# API Principal
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
# Configuración del worker
WORKER_INTERVAL=3000
PORT=3001

17
.env.example Normal file
View file

@ -0,0 +1,17 @@
cd ~/docker/OpenWA
cp .env.example .env
# API Principal
API_URL=http://localhost:3000/v1
# API KEY para conectar con el API principal
WORKER_KEY=uuid-generado-aqui
# OpenWA
OPENWA_URL=http://localhost:2785
OPENWA_API_KEY=tu-api-key-de-openwa
# Configuración del worker
WORKER_INTERVAL=30000
WORKER_LIMIT=5
PORT=3003

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

4
.prettierrc Normal file
View file

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

31
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,31 @@
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#fbed80",
"activityBar.background": "#fbed80",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#06b9a5",
"activityBarBadge.foreground": "#15202b",
"activityBarTop.activeBackground": "#fbed80",
"activityBarTop.background": "#fbed80",
"activityBarTop.foreground": "#15202b",
"activityBarTop.inactiveForeground": "#15202b99",
"commandCenter.border": "#15202b99",
"commandCenter.foreground": "#15202b",
"sash.hoverBorder": "#fbed80",
"statusBar.background": "#f9e64f",
"statusBar.debuggingBackground": "#f9e64f",
"statusBar.debuggingForeground": "#15202b",
"statusBar.foreground": "#15202b",
"statusBarItem.hoverBackground": "#f7df1e",
"statusBarItem.remoteBackground": "#059a89",
"statusBarItem.remoteForeground": "#15202b",
"titleBar.activeBackground": "#f9e64f",
"titleBar.activeForeground": "#15202b",
"titleBar.inactiveBackground": "#f9e64f99",
"titleBar.inactiveForeground": "#15202b99",
"activityBar.activeBorder": "#15202b",
"activityBarTop.activeBorder": "#15202b"
},
"peacock.color": "#f9e64f"
}

36
Dockerfile Normal file
View file

@ -0,0 +1,36 @@
# Stage 1: Builder
FROM node:22-alpine AS builder
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
# Stage 2: Production
FROM node:22-alpine
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod && pnpm store prune
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
USER nodejs
EXPOSE 3001
CMD ["node", "dist/main"]

114
README.md Normal file
View file

@ -0,0 +1,114 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ pnpm install
```
## Compile and run the project
```bash
# development
$ pnpm run start
# watch mode
$ pnpm run start:dev
# production mode
$ pnpm run start:prod
```
## Run tests
```bash
# unit tests
$ pnpm run test
# e2e tests
$ pnpm run test:e2e
# test coverage
$ pnpm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ pnpm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Observability
In production applications, observability is essential for understanding how your system behaves, detecting issues early, and maintaining reliable performance.
[NestJS Observe](https://observe.nestjs.com) automatically instruments your NestJS application, giving you deep visibility into your system with minimal setup:
- **Distributed tracing:** Follow requests across services and understand how they flow through your system.
- **Waterfall analysis:** Visualize request execution and identify slow operations, bottlenecks, and unexpected delays.
- **Performance analysis:** Analyze application performance in real time and quickly pinpoint areas that need optimization.
- **Metrics:** Track key application and infrastructure metrics to understand system health and performance trends.
- **Logging:** Centralize and correlate logs with traces and other telemetry to make debugging easier.
- **Error tracking:** Detect errors quickly and investigate their root causes with the surrounding context.
- **SLA monitoring:** Track service-level objectives and identify when your application is approaching or exceeding defined thresholds.
- **Alarms and alerts:** Set up alerts for critical errors, performance degradation, SLA violations, and other anomalies so your team can react quickly.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Auto-instrument your application with [NestJS Observer](https://observer.nestjs.com). Distributed tracing, metrics, and logging made easy. Error tracking and performance monitoring for your NestJS applications.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

6
dist/app.controller.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
import { AppService } from './app.service.js';
export declare class AppController {
private readonly appService;
constructor(appService: AppService);
getHello(): string;
}

32
dist/app.controller.js vendored Normal file
View file

@ -0,0 +1,32 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service.js';
let AppController = class AppController {
appService;
constructor(appService) {
this.appService = appService;
}
getHello() {
return this.appService.getHello();
}
};
__decorate([
Get(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", String)
], AppController.prototype, "getHello", null);
AppController = __decorate([
Controller(),
__metadata("design:paramtypes", [AppService])
], AppController);
export { AppController };
//# sourceMappingURL=app.controller.js.map

1
dist/app.controller.js.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"app.controller.js","sourceRoot":"","sources":["../src/app.controller.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGvC,IAAM,aAAa,GAAnB,MAAM,aAAa;IACK;IAA7B,YAA6B,UAAsB;QAAtB,eAAU,GAAV,UAAU,CAAY;IAAG,CAAC;IAGvD,QAAQ;QACN,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;IACpC,CAAC;CACF,CAAA;AAHC;IADC,GAAG,EAAE;;;;6CAGL;AANU,aAAa;IADzB,UAAU,EAAE;qCAE8B,UAAU;GADxC,aAAa,CAOzB"}

2
dist/app.module.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
export declare class AppModule {
}

21
dist/app.module.js vendored Normal file
View file

@ -0,0 +1,21 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { WorkerModule } from './worker/worker.module.js';
let AppModule = class AppModule {
};
AppModule = __decorate([
Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
WorkerModule,
],
})
], AppModule);
export { AppModule };
//# sourceMappingURL=app.module.js.map

1
dist/app.module.js.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAQlD,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,SAAS;IANrB,MAAM,CAAC;QACN,OAAO,EAAE;YACP,YAAY,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YACxC,YAAY;SACb;KACF,CAAC;GACW,SAAS,CAAG"}

3
dist/app.service.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
export declare class AppService {
getHello(): string;
}

17
dist/app.service.js vendored Normal file
View file

@ -0,0 +1,17 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Injectable } from '@nestjs/common';
let AppService = class AppService {
getHello() {
return 'Hello World!';
}
};
AppService = __decorate([
Injectable()
], AppService);
export { AppService };
//# sourceMappingURL=app.service.js.map

1
dist/app.service.js.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"app.service.js","sourceRoot":"","sources":["../src/app.service.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGrC,IAAM,UAAU,GAAhB,MAAM,UAAU;IACrB,QAAQ;QACN,OAAO,cAAc,CAAC;IACxB,CAAC;CACF,CAAA;AAJY,UAAU;IADtB,UAAU,EAAE;GACA,UAAU,CAItB"}

1
dist/main.d.ts vendored Normal file
View file

@ -0,0 +1 @@
export {};

10
dist/main.js vendored Normal file
View file

@ -0,0 +1,10 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const port = process.env.PORT ?? 3001;
await app.listen(port);
console.log(`Worker running on http://localhost:${port}`);
}
bootstrap();
//# sourceMappingURL=main.js.map

1
dist/main.js.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;IACtC,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACvB,OAAO,CAAC,GAAG,CAAC,sCAAsC,IAAI,EAAE,CAAC,CAAC;AAC5D,CAAC;AACD,SAAS,EAAE,CAAC"}

2
dist/worker/worker.module.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
export declare class WorkerModule {
}

20
dist/worker/worker.module.js vendored Normal file
View file

@ -0,0 +1,20 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { HttpModule } from '@nestjs/axios';
import { WorkerService } from './worker.service.js';
let WorkerModule = class WorkerModule {
};
WorkerModule = __decorate([
Module({
imports: [ConfigModule, HttpModule],
providers: [WorkerService],
})
], WorkerModule);
export { WorkerModule };
//# sourceMappingURL=worker.module.js.map

1
dist/worker/worker.module.js.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"worker.module.js","sourceRoot":"","sources":["../../src/worker/worker.module.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAM7C,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,YAAY;IAJxB,MAAM,CAAC;QACN,OAAO,EAAE,CAAC,YAAY,EAAE,UAAU,CAAC;QACnC,SAAS,EAAE,CAAC,aAAa,CAAC;KAC3B,CAAC;GACW,YAAY,CAAG"}

29
dist/worker/worker.service.d.ts vendored Normal file
View file

@ -0,0 +1,29 @@
import { OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
export declare class WorkerService implements OnModuleInit, OnModuleDestroy {
private config;
private http;
private readonly logger;
private isRunning;
private readonly apiUrl;
private readonly workerKey;
private readonly openwaUrl;
private readonly openwaApiKey;
private readonly intervalMs;
constructor(config: ConfigService, http: HttpService);
onModuleInit(): void;
onModuleDestroy(): void;
private timestamp;
private procesarCiclo;
private procesar;
private obtenerPendiente;
private procesarMensaje;
private calcularDelayEscritura;
private calcularDelayEntreMensajes;
private delay;
private setTyping;
private enviarTexto;
private enviarImagen;
private actualizarEstado;
}

216
dist/worker/worker.service.js vendored Normal file
View file

@ -0,0 +1,216 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var WorkerService_1;
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
let WorkerService = WorkerService_1 = class WorkerService {
config;
http;
logger = new Logger(WorkerService_1.name);
isRunning = true;
apiUrl;
workerKey;
openwaUrl;
openwaApiKey;
intervalMs;
constructor(config, http) {
this.config = config;
this.http = http;
this.apiUrl = this.config.get('API_URL') || 'http://localhost:3000/v1';
this.workerKey = this.config.get('WORKER_KEY') || '';
this.openwaUrl = this.config.get('OPENWA_URL') || 'http://localhost:2785';
this.openwaApiKey = this.config.get('OPENWA_API_KEY') || '';
this.intervalMs = Number(this.config.get('WORKER_INTERVAL')) || 30000;
}
onModuleInit() {
this.logger.log(`Worker iniciado. Intervalo entre mensajes: ${this.intervalMs}ms`);
this.procesarCiclo();
}
onModuleDestroy() {
this.isRunning = false;
this.logger.log('Worker detenido');
}
timestamp() {
return new Date().toLocaleTimeString('es-BO', { hour12: false });
}
async procesarCiclo() {
if (!this.isRunning)
return;
await this.procesar();
const delayMs = this.calcularDelayEntreMensajes();
const delaySeg = (delayMs / 1000).toFixed(1);
this.logger.log(`[${this.timestamp()}] ⏳ Esperando ${delaySeg}s antes del siguiente mensaje...`);
await this.delay(delayMs);
setTimeout(() => this.procesarCiclo(), 0);
}
async procesar() {
const mensaje = await this.obtenerPendiente();
if (!mensaje) {
this.logger.log(`[${this.timestamp()}] No hay mensajes pendientes`);
return;
}
await this.procesarMensaje(mensaje);
}
async obtenerPendiente() {
try {
const response = await firstValueFrom(this.http.get(`${this.apiUrl}/mensaje/pendientes`, {
params: { limit: 1 },
headers: { 'x-worker-key': this.workerKey },
}));
const data = response.data?.data;
return data && data.length > 0 ? data[0] : null;
}
catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`[${this.timestamp()}] Error obteniendo mensaje pendiente: ${message}`);
return null;
}
}
async procesarMensaje(mensaje) {
const { idMensajeWhatsApp, telefonoDestino, sessionId, tipo, mensaje1, mensaje2, mensaje3, urlQR } = mensaje;
const inicio = Date.now();
this.logger.log(`[${this.timestamp()}] ▶ Procesando mensaje ${idMensajeWhatsApp}${telefonoDestino} (${tipo})`);
try {
if (!sessionId) {
await this.actualizarEstado(idMensajeWhatsApp, 'ERROR', 'No sessionId configurado');
return;
}
const chatId = `${telefonoDestino}@c.us`;
const mensajes = [mensaje1, mensaje2, mensaje3].filter((m) => Boolean(m));
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`);
}
else {
await this.enviarTexto(sessionId, chatId, texto1);
this.logger.log(`[${this.timestamp()}] ✉️ Mensaje 1 enviado INMEDIATO`);
}
}
const texto1ParaDelay = mensajes[0] || '';
const delay1 = this.calcularDelayEscritura(texto1ParaDelay.length);
const delay1Seg = (delay1 / 1000).toFixed(1);
this.logger.log(`[${this.timestamp()}] ⏳ Delay1 calculado: ${delay1Seg}s (para usar después)`);
for (let i = 1; i < mensajes.length; i++) {
const texto = mensajes[i];
const delayMsg = this.calcularDelayEscritura(texto.length);
const delayMsgSeg = (delayMsg / 1000).toFixed(1);
await this.setTyping(sessionId, chatId);
this.logger.log(`[${this.timestamp()}] ⌨️ Typing mensaje ${i + 1}...`);
this.logger.log(`[${this.timestamp()}] ⏳ Esperando ${delayMsgSeg}s (${texto.length} chars)`);
await this.delay(delayMsg);
await this.enviarTexto(sessionId, chatId, texto);
this.logger.log(`[${this.timestamp()}] ✉️ Mensaje ${i + 1} enviado`);
if (i < mensajes.length - 1) {
const delayEntre = this.calcularDelayEntreMensajes();
const delayEntreSeg = (delayEntre / 1000).toFixed(1);
this.logger.log(`[${this.timestamp()}] ⏳ Esperando ${delayEntreSeg}s antes del siguiente mensaje...`);
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`);
}
catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
const responseMessage = error?.response?.data?.message || error?.response?.data?.error || '';
const fullError = responseMessage ? `${message}${responseMessage}` : message;
await this.actualizarEstado(idMensajeWhatsApp, 'ERROR', fullError);
const totalSeg = ((Date.now() - inicio) / 1000).toFixed(1);
this.logger.error(`[${this.timestamp()}] ❌ Error en ${idMensajeWhatsApp}: ${fullError} (${totalSeg}s)`);
}
}
calcularDelayEscritura(caracteres) {
const caracteresPorMinuto = 200;
const base = (caracteres / caracteresPorMinuto) * 60 * 1000;
const random = 1 + (Math.random() * 0.6 - 0.3);
const reaccion = 500 + Math.random() * 1000;
const delay = base * random + reaccion;
return Math.min(Math.max(delay, 3000), 120000);
}
calcularDelayEntreMensajes() {
const base = 3000;
const random = 1 + (Math.random() * 0.6 - 0.3);
return Math.min(Math.max(base * random, 2000), 8000);
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async setTyping(sessionId, chatId) {
try {
await firstValueFrom(this.http.post(`${this.openwaUrl}/api/sessions/${sessionId}/chat-state`, { chatId, state: 'composing' }, {
headers: {
'X-API-Key': this.openwaApiKey,
'Content-Type': 'application/json',
},
}));
}
catch {
}
}
async enviarTexto(sessionId, chatId, text) {
const response = await firstValueFrom(this.http.post(`${this.openwaUrl}/api/sessions/${sessionId}/messages/send-text`, { chatId, text }, {
headers: {
'X-API-Key': this.openwaApiKey,
'Content-Type': 'application/json',
},
}));
return response.data;
}
async enviarImagen(sessionId, chatId, imageUrl, caption) {
const fullUrl = imageUrl.startsWith('http')
? imageUrl
: `http://localhost:3000${imageUrl}`;
const response = await firstValueFrom(this.http.get(fullUrl, { responseType: 'arraybuffer' }));
const base64Data = Buffer.from(response.data).toString('base64');
const dataUrl = `data:image/png;base64,${base64Data}`;
const result = await firstValueFrom(this.http.post(`${this.openwaUrl}/api/sessions/${sessionId}/messages/send-image`, {
chatId,
base64: dataUrl,
caption,
mimetype: 'image/png',
filename: 'qr.png',
}, {
headers: {
'X-API-Key': this.openwaApiKey,
'Content-Type': 'application/json',
},
}));
return result.data;
}
async actualizarEstado(id, estado, error) {
try {
await firstValueFrom(this.http.put(`${this.apiUrl}/mensaje/${id}/estado`, {
estado,
error: error || null,
fechaEnvio: new Date().toISOString(),
}, {
headers: { 'x-worker-key': this.workerKey },
}));
}
catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
this.logger.error(`[${this.timestamp()}] Error actualizando estado del mensaje ${id}: ${message}`);
}
}
};
WorkerService = WorkerService_1 = __decorate([
Injectable(),
__metadata("design:paramtypes", [ConfigService,
HttpService])
], WorkerService);
export { WorkerService };
//# sourceMappingURL=worker.service.js.map

1
dist/worker/worker.service.js.map vendored Normal file

File diff suppressed because one or more lines are too long

8
nest-cli.json Normal file
View file

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

10
oxlint.json Normal file
View file

@ -0,0 +1,10 @@
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/crates/oxc_linter/src/rules.rs",
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-floating-promises": "warn"
},
"env": {
"node": true
}
}

51
package.json Normal file
View file

@ -0,0 +1,51 @@
{
"name": "app-paqueteria-worker",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"type": "module",
"scripts": {
"build": "nest build",
"deploy": "nest deploy",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "oxlint src/ test/",
"test": "vitest run",
"test:watch": "vitest",
"test:cov": "vitest run --coverage",
"test:debug": "vitest --inspect-brk --no-file-parallelism",
"test:e2e": "vitest run --config ./vitest.config.e2e.ts"
},
"dependencies": {
"@nestjs/axios": "^12.0.0",
"@nestjs/common": "^12.0.1",
"@nestjs/config": "^12.0.0",
"@nestjs/core": "^12.0.1",
"@nestjs/platform-express": "^12.0.1",
"axios": "^1.20.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^12.0.0",
"@nestjs/mau": "^0.2.6",
"@nestjs/schematics": "^12.0.0",
"@nestjs/testing": "^12.0.1",
"@types/express": "^5.0.0",
"@types/node": "^24.0.0",
"@types/supertest": "^7.0.0",
"@vitest/coverage-v8": "^4.1.2",
"oxlint": "^1.58.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"typescript": "^6.0.2",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.1.2"
}
}

3582
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

12
src/app.controller.ts Normal file
View file

@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service.js';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

11
src/app.module.ts Normal file
View file

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { WorkerModule } from './worker/worker.module.js';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
WorkerModule,
],
})
export class AppModule {}

8
src/app.service.ts Normal file
View file

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

10
src/main.ts Normal file
View file

@ -0,0 +1,10 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const port = process.env.PORT ?? 3001;
await app.listen(port);
console.log(`Worker running on http://localhost:${port}`);
}
bootstrap();

View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { HttpModule } from '@nestjs/axios';
import { WorkerService } from './worker.service.js';
@Module({
imports: [ConfigModule, HttpModule],
providers: [WorkerService],
})
export class WorkerModule {}

View file

@ -0,0 +1,278 @@
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
interface MensajePendiente {
idMensajeWhatsApp: string;
idPaquete: string;
telefonoDestino: string;
tipo: string;
mensaje1: string;
mensaje2?: string;
mensaje3?: string;
urlQR?: string;
sessionId: string;
}
@Injectable()
export class WorkerService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(WorkerService.name);
private isRunning = true;
private readonly apiUrl: string;
private readonly workerKey: string;
private readonly openwaUrl: string;
private readonly openwaApiKey: string;
private readonly intervalMs: number;
constructor(
private config: ConfigService,
private http: HttpService,
) {
this.apiUrl = this.config.get('API_URL') || 'http://localhost:3000/v1';
this.workerKey = this.config.get('WORKER_KEY') || '';
this.openwaUrl = this.config.get('OPENWA_URL') || 'http://localhost:2785';
this.openwaApiKey = this.config.get('OPENWA_API_KEY') || '';
this.intervalMs = Number(this.config.get('WORKER_INTERVAL')) || 30000;
}
onModuleInit() {
this.logger.log(`Worker iniciado. Intervalo entre mensajes: ${this.intervalMs}ms`);
this.procesarCiclo();
}
onModuleDestroy() {
this.isRunning = false;
this.logger.log('Worker detenido');
}
private timestamp(): string {
return new Date().toLocaleTimeString('es-BO', { hour12: false });
}
private async procesarCiclo() {
if (!this.isRunning) return;
await this.procesar();
// Delay entre mensajes
const delayMs = this.calcularDelayEntreMensajes();
const delaySeg = (delayMs / 1000).toFixed(1);
this.logger.log(`[${this.timestamp()}] ⏳ Esperando ${delaySeg}s antes del siguiente mensaje...`);
await this.delay(delayMs);
setTimeout(() => this.procesarCiclo(), 0);
}
private async procesar() {
const mensaje = await this.obtenerPendiente();
if (!mensaje) {
this.logger.log(`[${this.timestamp()}] No hay mensajes pendientes`);
return;
}
await this.procesarMensaje(mensaje);
}
private async obtenerPendiente(): Promise<MensajePendiente | null> {
try {
const response = await firstValueFrom(
this.http.get(`${this.apiUrl}/mensaje/pendientes`, {
params: { limit: 1 },
headers: { 'x-worker-key': this.workerKey },
}),
);
const data = response.data?.data;
return data && data.length > 0 ? data[0] : null;
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`[${this.timestamp()}] Error obteniendo mensaje pendiente: ${message}`);
return null;
}
}
private async procesarMensaje(mensaje: MensajePendiente) {
const { idMensajeWhatsApp, telefonoDestino, sessionId, tipo, mensaje1, mensaje2, mensaje3, urlQR } = mensaje;
const inicio = Date.now();
this.logger.log(`[${this.timestamp()}] ▶ Procesando mensaje ${idMensajeWhatsApp}${telefonoDestino} (${tipo})`);
try {
if (!sessionId) {
await this.actualizarEstado(idMensajeWhatsApp, 'ERROR', 'No sessionId configurado');
return;
}
const chatId = `${telefonoDestino}@c.us`;
const mensajes: string[] = [mensaje1, mensaje2, mensaje3].filter((m): m is string => Boolean(m));
// ── PASO 1: Enviar mensaje1 + QR INMEDIATO (sin delay) ──
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`);
} else {
await this.enviarTexto(sessionId, chatId, texto1);
this.logger.log(`[${this.timestamp()}] ✉️ Mensaje 1 enviado INMEDIATO`);
}
}
// ── PASO 2: Calcular y guardar delay1 para usarlo al final ──
const texto1ParaDelay = mensajes[0] || '';
const delay1 = this.calcularDelayEscritura(texto1ParaDelay.length);
const delay1Seg = (delay1 / 1000).toFixed(1);
this.logger.log(`[${this.timestamp()}] ⏳ Delay1 calculado: ${delay1Seg}s (para usar después)`);
// ── PASOS 3-6: Enviar mensajes 2 y 3 con delays y typing ──
for (let i = 1; i < mensajes.length; i++) {
const texto = mensajes[i];
// Calcular delay para este mensaje
const delayMsg = this.calcularDelayEscritura(texto.length);
const delayMsgSeg = (delayMsg / 1000).toFixed(1);
// Typing indicator + esperar delay
await this.setTyping(sessionId, chatId);
this.logger.log(`[${this.timestamp()}] ⌨️ Typing mensaje ${i + 1}...`);
this.logger.log(`[${this.timestamp()}] ⏳ Esperando ${delayMsgSeg}s (${texto.length} chars)`);
await this.delay(delayMsg);
// Enviar mensaje
await this.enviarTexto(sessionId, chatId, texto);
this.logger.log(`[${this.timestamp()}] ✉️ Mensaje ${i + 1} enviado`);
// Delay entre mensajes (si hay más)
if (i < mensajes.length - 1) {
const delayEntre = this.calcularDelayEntreMensajes();
const delayEntreSeg = (delayEntre / 1000).toFixed(1);
this.logger.log(`[${this.timestamp()}] ⏳ Esperando ${delayEntreSeg}s antes del siguiente mensaje...`);
await this.delay(delayEntre);
}
}
// ── 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
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`);
} catch (error: any) {
const message = error instanceof Error ? error.message : 'Unknown error';
const responseMessage = error?.response?.data?.message || error?.response?.data?.error || '';
const fullError = responseMessage ? `${message}${responseMessage}` : message;
await this.actualizarEstado(idMensajeWhatsApp, 'ERROR', fullError);
const totalSeg = ((Date.now() - inicio) / 1000).toFixed(1);
this.logger.error(`[${this.timestamp()}] ❌ Error en ${idMensajeWhatsApp}: ${fullError} (${totalSeg}s)`);
}
}
private calcularDelayEscritura(caracteres: number): number {
const caracteresPorMinuto = 200;
const base = (caracteres / caracteresPorMinuto) * 60 * 1000;
const random = 1 + (Math.random() * 0.6 - 0.3);
const reaccion = 500 + Math.random() * 1000;
const delay = base * random + reaccion;
return Math.min(Math.max(delay, 3000), 120000); // 3s-2min
}
private calcularDelayEntreMensajes(): number {
const base = 3000; // 3s base entre mensajes
const random = 1 + (Math.random() * 0.6 - 0.3); // ±30%
return Math.min(Math.max(base * random, 2000), 8000); // 2s-8s
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
private async setTyping(sessionId: string, chatId: string) {
try {
await firstValueFrom(
this.http.post(
`${this.openwaUrl}/api/sessions/${sessionId}/chat-state`,
{ chatId, state: 'composing' },
{
headers: {
'X-API-Key': this.openwaApiKey,
'Content-Type': 'application/json',
},
},
),
);
} catch {
// Silenciar errores de typing — no es crítico
}
}
private async enviarTexto(sessionId: string, chatId: string, text: string) {
const response = await firstValueFrom(
this.http.post(
`${this.openwaUrl}/api/sessions/${sessionId}/messages/send-text`,
{ chatId, text },
{
headers: {
'X-API-Key': this.openwaApiKey,
'Content-Type': 'application/json',
},
},
),
);
return response.data;
}
private async enviarImagen(sessionId: string, chatId: string, imageUrl: string, caption: string) {
const fullUrl = imageUrl.startsWith('http')
? imageUrl
: `http://localhost:3000${imageUrl}`;
const response = await firstValueFrom(this.http.get(fullUrl, { responseType: 'arraybuffer' }));
const base64Data = Buffer.from(response.data).toString('base64');
const dataUrl = `data:image/png;base64,${base64Data}`;
const result = await firstValueFrom(
this.http.post(
`${this.openwaUrl}/api/sessions/${sessionId}/messages/send-image`,
{
chatId,
base64: dataUrl,
caption,
mimetype: 'image/png',
filename: 'qr.png',
},
{
headers: {
'X-API-Key': this.openwaApiKey,
'Content-Type': 'application/json',
},
},
),
);
return result.data;
}
private async actualizarEstado(id: string, estado: string, error?: string) {
try {
await firstValueFrom(
this.http.put(
`${this.apiUrl}/mensaje/${id}/estado`,
{
estado,
error: error || null,
fechaEnvio: new Date().toISOString(),
},
{
headers: { 'x-worker-key': this.workerKey },
},
),
);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Unknown error';
this.logger.error(`[${this.timestamp()}] Error actualizando estado del mensaje ${id}: ${message}`);
}
}
}

29
test/app.e2e-spec.ts Normal file
View file

@ -0,0 +1,29 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module.js';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
afterEach(async () => {
await app.close();
});
});

8
tsconfig.build.json Normal file
View file

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
},
"include": ["src"],
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

File diff suppressed because one or more lines are too long

22
tsconfig.json Normal file
View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"incremental": true,
"skipLibCheck": true,
"strict": true,
"strictPropertyInitialization": false,
"types": ["vitest/globals", "node"]
}
}

11
vitest.config.e2e.ts Normal file
View file

@ -0,0 +1,11 @@
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tsconfigPaths()],
test: {
globals: true,
root: './',
include: ['**/*.e2e-spec.ts'],
},
});

13
vitest.config.ts Normal file
View file

@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
// Resolves the path aliases declared in tsconfig.json, including the ones
// added by `nest g library`.
plugins: [tsconfigPaths()],
test: {
globals: true,
root: './',
include: ['**/*.spec.ts'],
},
});