First Commit
This commit is contained in:
commit
e774dec930
43 changed files with 4682 additions and 0 deletions
15
.dockerignore
Normal file
15
.dockerignore
Normal 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
13
.env
Normal 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
17
.env.example
Normal 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
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
31
.vscode/settings.json
vendored
Normal file
31
.vscode/settings.json
vendored
Normal 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
36
Dockerfile
Normal 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
114
README.md
Normal 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>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](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
6
dist/app.controller.d.ts
vendored
Normal 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
32
dist/app.controller.js
vendored
Normal 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
1
dist/app.controller.js.map
vendored
Normal 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
2
dist/app.module.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export declare class AppModule {
|
||||
}
|
||||
21
dist/app.module.js
vendored
Normal file
21
dist/app.module.js
vendored
Normal 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
1
dist/app.module.js.map
vendored
Normal 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
3
dist/app.service.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export declare class AppService {
|
||||
getHello(): string;
|
||||
}
|
||||
17
dist/app.service.js
vendored
Normal file
17
dist/app.service.js
vendored
Normal 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
1
dist/app.service.js.map
vendored
Normal 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
1
dist/main.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
export {};
|
||||
10
dist/main.js
vendored
Normal file
10
dist/main.js
vendored
Normal 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
1
dist/main.js.map
vendored
Normal 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
2
dist/worker/worker.module.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export declare class WorkerModule {
|
||||
}
|
||||
20
dist/worker/worker.module.js
vendored
Normal file
20
dist/worker/worker.module.js
vendored
Normal 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
1
dist/worker/worker.module.js.map
vendored
Normal 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
29
dist/worker/worker.service.d.ts
vendored
Normal 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
216
dist/worker/worker.service.js
vendored
Normal 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
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
8
nest-cli.json
Normal 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
10
oxlint.json
Normal 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
51
package.json
Normal 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
3582
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
22
src/app.controller.spec.ts
Normal file
22
src/app.controller.spec.ts
Normal 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
12
src/app.controller.ts
Normal 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
11
src/app.module.ts
Normal 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
8
src/app.service.ts
Normal 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
10
src/main.ts
Normal 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();
|
||||
10
src/worker/worker.module.ts
Normal file
10
src/worker/worker.module.ts
Normal 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 {}
|
||||
278
src/worker/worker.service.ts
Normal file
278
src/worker/worker.service.ts
Normal 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
29
test/app.e2e-spec.ts
Normal 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
8
tsconfig.build.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
1
tsconfig.build.tsbuildinfo
Normal file
1
tsconfig.build.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
22
tsconfig.json
Normal file
22
tsconfig.json
Normal 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
11
vitest.config.e2e.ts
Normal 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
13
vitest.config.ts
Normal 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'],
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue