Merge branch 'release/1.6.1'

This commit is contained in:
Davidson Gomes 2023-12-22 11:44:12 -03:00
commit 39abf03746
33 changed files with 660 additions and 167 deletions

View File

@ -0,0 +1,38 @@
---
name: "[EN] Bug report"
about: Create a report to help us improve
title: "[EN][BUG]"
labels: bug
assignees: ''
---
### Title: [Brief Description of the Bug]
#### Description:
Describe in detail the problem you encountered. Include any relevant context that may help understand the origin of the bug.
#### Steps to Reproduce:
1. List the steps necessary to reproduce the problem.
2. Try to be as specific as possible.
3. If the problem occurs in a specific scenario, describe it here.
#### Expected Behavior:
Describe what you expected to happen when following the steps above.
#### Current Behavior:
Explain what actually happens when you follow the steps above.
#### Screenshots/Videos:
If possible, add screenshots or videos illustrating the problem. This can be extremely helpful in understanding the issue.
#### Environment:
- **Server:** [e.g., Ubuntu 18.04]
- **API Version:** [e.g., 1.5.4]
- **Other Hardware/Software Specifications:** [e.g., CPU, GPU]
#### Submitting Logs:
Please attach logs that may be related to the problem. If the logs contain sensitive information, consider sending them privately to one of the project maintainers.
#### Additional Notes:
Include here any other information that you think might be useful in understanding or resolving the bug.

View File

@ -0,0 +1,28 @@
---
name: "[EN] Feature request"
about: Suggest an idea for the API
title: "[EN][FEAT]"
labels: enhancement
assignees: ''
---
### Title: [Brief Description of Feature Request]
#### Detailed Description:
Clearly and in detail, describe the functionality you wish to be implemented. Explain how this fits into the context of the project.
#### Rationale:
Explain why this functionality would be useful for the project. This helps in understanding the importance and priority of the request.
#### Usage Examples:
Provide specific examples of how this feature could be used. This can include scenarios or use cases where the feature would be particularly beneficial.
#### Possible Implementations:
If you have ideas on how this feature might be implemented, please share them here. This is not mandatory but can be helpful for the development team.
#### Impact on the Project:
Discuss how this new feature could impact other parts of the project, if applicable.
#### Additional Notes:
Any other information you believe is relevant to your request.

View File

@ -0,0 +1,38 @@
---
name: "[PT] Reportar bug"
about: Reportar um problema
title: "[PT][BUG]"
labels: bug
assignees: ''
---
### Título: [Breve Descrição do Bug]
#### Descrição:
Descreva detalhadamente o problema que você encontrou. Inclua qualquer contexto relevante que possa ajudar a entender a origem do bug.
#### Passos para Reproduzir:
1. Liste os passos necessários para reproduzir o problema.
2. Tente ser o mais específico possível.
3. Se o problema ocorrer em um cenário específico, descreva-o aqui.
#### Comportamento Esperado:
Descreva o que você esperava que acontecesse quando seguisse os passos acima.
#### Comportamento Atual:
Explique o que realmente acontece quando você segue os passos acima.
#### Capturas de Tela/Vídeos:
Se possível, adicione capturas de tela ou vídeos que ilustrem o problema. Isso pode ser extremamente útil para entender o problema.
#### Ambiente:
- **Servidor:** [ex: Ubuntu 18.04]
- **Versão da API:** [ex: 1.5.4]
- **Outras Especificações de Hardware/Software:** [ex: CPU, GPU]
#### Envio de Logs:
Por favor, anexe os logs que possam estar relacionados ao problema. Se os logs contiverem informações sensíveis, considere enviá-los de forma privada para um dos mantenedores do projeto.
#### Notas Adicionais:
Inclua aqui qualquer outra informação que você ache que possa ser útil para entender ou resolver o bug.

View File

@ -0,0 +1,28 @@
---
name: "[PT] Solicitar recurso"
about: Sugira novos recursos para a API
title: "[PT][FEAT]"
labels: enhancement
assignees: ''
---
### Título: [Breve Descrição da Solicitação de Recurso]
#### Descrição Detalhada:
Descreva claramente e em detalhes a funcionalidade que você deseja que seja implementada. Explique como isso se encaixa no contexto do projeto.
#### Racional:
Explique por que essa funcionalidade seria útil para o projeto. Isso ajuda a entender a importância e a prioridade da solicitação.
#### Exemplos de Uso:
Forneça exemplos específicos de como essa funcionalidade poderia ser utilizada. Isso pode incluir cenários ou casos de uso onde a funcionalidade seria particularmente benéfica.
#### Possíveis Implementações:
Se você tem ideias sobre como essa funcionalidade pode ser implementada, por favor, compartilhe-as aqui. Isso não é obrigatório, mas pode ser útil para a equipe de desenvolvimento.
#### Impacto no Projeto:
Discuta como essa nova funcionalidade poderia impactar outras partes do projeto, se aplicável.
#### Notas Adicionais:
Qualquer outra informação que você acredita ser relevante para a sua solicitação.

1
.gitignore vendored
View File

@ -39,6 +39,7 @@ docker-compose.yaml
/test/ /test/
/src/env.yml /src/env.yml
/store /store
*.env
/temp/* /temp/*

View File

@ -1,12 +1,26 @@
# 1.6.1 (develop) # 1.6.1 (2023-12-22 11:43)
### Fixed ### Fixed
* Fixed Lid Messages * Fixed Lid Messages
* Fixed sending variables to typebot
* Fixed sending variables from typebot
* Correction sending s3/minio media to chatwoot and typebot
* Fixed the problem with typebot closing at the end of the flow, now this is optional with the TYPEBOT_KEEP_OPEN variable
* Fixed chatwoot Bold, Italic and Underline formatting using Regex
* Added the sign_delimiter property to the Chatwoot configuration, allowing you to set a different delimiter for the signature. Default when not defined \n
* Include instance Id field in the instance configuration
* Fixed the pairing code
* Adjusts in typebot
* Fix the problem when disconnecting the instance and connecting again using mongodb
* Options to disable docs and manager
* When deleting a message in whatsapp, delete the message in chatwoot too
# 1.6.0 (2023-12-12 17:24) # 1.6.0 (2023-12-12 17:24)
### Feature ### Feature
* Added AWS SQS Integration * Added AWS SQS Integration
* Added support for new typebot API * Added support for new typebot API
* Added endpoint sendPresence * Added endpoint sendPresence
@ -24,7 +38,6 @@
* Fix workaround to manage param data as an array in mongodb * Fix workaround to manage param data as an array in mongodb
* Removed await from webhook when sending a message * Removed await from webhook when sending a message
* Update typebot.service.ts - element.underline change ~ for * * Update typebot.service.ts - element.underline change ~ for *
* Adjusts in proxy
* Removed api restart on receiving an error * Removed api restart on receiving an error
* Fixes in mongodb and chatwoot * Fixes in mongodb and chatwoot
* Adjusted return from queries in mongodb * Adjusted return from queries in mongodb
@ -36,8 +49,8 @@
### Integrations ### Integrations
- Chatwoot: v3.3.1 * Chatwoot: v3.3.1
- Typebot: v2.20.0 * Typebot: v2.20.0
# 1.5.4 (2023-10-09 20:43) # 1.5.4 (2023-10-09 20:43)
@ -116,9 +129,9 @@
### Integrations ### Integrations
- Chatwoot: v2.18.0 - v3.0.0 * Chatwoot: v2.18.0 - v3.0.0
- Typebot: v2.16.0 * Typebot: v2.16.0
- Manager Evolution API * Manager Evolution API
# 1.4.8 (2023-07-27 10:27) # 1.4.8 (2023-07-27 10:27)
@ -206,7 +219,7 @@
### Integrations ### Integrations
- Chatwoot: v2.18.0 - v3.0.0 (Beta) * Chatwoot: v2.18.0 - v3.0.0 (Beta)
# 1.3.2 (2023-07-21 17:19) # 1.3.2 (2023-07-21 17:19)
@ -222,7 +235,7 @@
### Integrations ### Integrations
- Chatwoot: v2.18.0 * Chatwoot: v2.18.0
# 1.3.1 (2023-07-20 07:48) # 1.3.1 (2023-07-20 07:48)
@ -232,7 +245,7 @@
### Integrations ### Integrations
- Chatwoot: v2.18.0 * Chatwoot: v2.18.0
# 1.3.0 (2023-07-19 11:33) # 1.3.0 (2023-07-19 11:33)
@ -269,7 +282,7 @@
### Integrations ### Integrations
- Chatwoot: v2.18.0 * Chatwoot: v2.18.0
# 1.2.2 (2023-07-15 09:36) # 1.2.2 (2023-07-15 09:36)
@ -280,7 +293,7 @@
### Integrations ### Integrations
- Chatwoot: v2.18.0 * Chatwoot: v2.18.0
# 1.2.1 (2023-07-14 19:04) # 1.2.1 (2023-07-14 19:04)

View File

@ -107,6 +107,7 @@ QRCODE_COLOR=#198754
# old | latest # old | latest
TYPEBOT_API_VERSION=latest TYPEBOT_API_VERSION=latest
TYPEBOT_KEEP_OPEN=false
# Defines an authentication type for the api # Defines an authentication type for the api
# We recommend using the apikey because it will allow you to use a custom token, # We recommend using the apikey because it will allow you to use a custom token,

View File

@ -1,6 +1,6 @@
FROM node:20.7.0-alpine FROM node:20.7.0-alpine AS builder
LABEL version="1.6.0" description="Api to control whatsapp features through http requests." LABEL version="1.6.1" description="Api to control whatsapp features through http requests."
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
LABEL contact="contato@agenciadgcode.com" LABEL contact="contato@agenciadgcode.com"
@ -11,9 +11,19 @@ WORKDIR /evolution
COPY ./package.json . COPY ./package.json .
RUN npm install
COPY . .
RUN npm run build
FROM node:20.7.0-alpine AS final
ENV TZ=America/Sao_Paulo ENV TZ=America/Sao_Paulo
ENV DOCKER_ENV=true ENV DOCKER_ENV=true
ENV SERVER_TYPE=http
ENV SERVER_PORT=8080
ENV SERVER_URL=http://localhost:8080 ENV SERVER_URL=http://localhost:8080
ENV CORS_ORIGIN=* ENV CORS_ORIGIN=*
@ -68,6 +78,8 @@ ENV WEBHOOK_GLOBAL_ENABLED=false
ENV WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false ENV WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false
ENV WEBHOOK_EVENTS_APPLICATION_STARTUP=false ENV WEBHOOK_EVENTS_APPLICATION_STARTUP=false
ENV WEBHOOK_EVENTS_INSTANCE_CREATE=false
ENV WEBHOOK_EVENTS_INSTANCE_DELETE=false
ENV WEBHOOK_EVENTS_QRCODE_UPDATED=true ENV WEBHOOK_EVENTS_QRCODE_UPDATED=true
ENV WEBHOOK_EVENTS_MESSAGES_SET=true ENV WEBHOOK_EVENTS_MESSAGES_SET=true
ENV WEBHOOK_EVENTS_MESSAGES_UPSERT=true ENV WEBHOOK_EVENTS_MESSAGES_UPSERT=true
@ -122,10 +134,8 @@ ENV AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID=1
ENV AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN=123456 ENV AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN=123456
ENV AUTHENTICATION_INSTANCE_CHATWOOT_URL=<url> ENV AUTHENTICATION_INSTANCE_CHATWOOT_URL=<url>
RUN npm install WORKDIR /evolution
COPY . . COPY --from=builder /evolution .
RUN npm run build
CMD [ "node", "./dist/src/main.js" ] CMD [ "node", "./dist/src/main.js" ]

View File

@ -3,7 +3,7 @@ version: '3.3'
services: services:
api: api:
container_name: evolution_api container_name: evolution_api
image: davidsongomes/evolution-api:latest image: atendai/evolution-api:latest
restart: always restart: always
ports: ports:
- 8080:8080 - 8080:8080

View File

@ -1,6 +1,6 @@
{ {
"name": "evolution-api", "name": "evolution-api",
"version": "1.6.0", "version": "1.6.1",
"description": "Rest api for communication with WhatsApp", "description": "Rest api for communication with WhatsApp",
"main": "./dist/src/main.js", "main": "./dist/src/main.js",
"scripts": { "scripts": {
@ -46,7 +46,7 @@
"@figuro/chatwoot-sdk": "^1.1.16", "@figuro/chatwoot-sdk": "^1.1.16",
"@hapi/boom": "^10.0.1", "@hapi/boom": "^10.0.1",
"@sentry/node": "^7.59.2", "@sentry/node": "^7.59.2",
"@whiskeysockets/baileys": "^6.5.0", "@whiskeysockets/baileys": "github:PurpShell/Baileys#combined",
"amqplib": "^0.10.3", "amqplib": "^0.10.3",
"aws-sdk": "^2.1499.0", "aws-sdk": "^2.1499.0",
"axios": "^1.3.5", "axios": "^1.3.5",
@ -56,7 +56,7 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"eventemitter2": "^6.4.9", "eventemitter2": "^6.4.9",
"evolution-manager": "^0.4.4", "evolution-manager": "^0.4.11",
"exiftool-vendored": "^22.0.0", "exiftool-vendored": "^22.0.0",
"express": "^4.18.2", "express": "^4.18.2",
"express-async-errors": "^3.1.1", "express-async-errors": "^3.1.1",

View File

@ -3,7 +3,13 @@ import { readFileSync } from 'fs';
import { load } from 'js-yaml'; import { load } from 'js-yaml';
import { join } from 'path'; import { join } from 'path';
export type HttpServer = { TYPE: 'http' | 'https'; PORT: number; URL: string }; export type HttpServer = {
TYPE: 'http' | 'https';
PORT: number;
URL: string;
DISABLE_DOCS: boolean;
DISABLE_MANAGER: boolean;
};
export type HttpMethods = 'POST' | 'GET' | 'PUT' | 'DELETE'; export type HttpMethods = 'POST' | 'GET' | 'PUT' | 'DELETE';
export type Cors = { export type Cors = {
@ -80,6 +86,8 @@ export type Websocket = {
export type EventsWebhook = { export type EventsWebhook = {
APPLICATION_STARTUP: boolean; APPLICATION_STARTUP: boolean;
INSTANCE_CREATE: boolean;
INSTANCE_DELETE: boolean;
QRCODE_UPDATED: boolean; QRCODE_UPDATED: boolean;
MESSAGES_SET: boolean; MESSAGES_SET: boolean;
MESSAGES_UPSERT: boolean; MESSAGES_UPSERT: boolean;
@ -128,7 +136,7 @@ export type SslConf = { PRIVKEY: string; FULLCHAIN: string };
export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook }; export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook };
export type ConfigSessionPhone = { CLIENT: string; NAME: string }; export type ConfigSessionPhone = { CLIENT: string; NAME: string };
export type QrCode = { LIMIT: number; COLOR: string }; export type QrCode = { LIMIT: number; COLOR: string };
export type Typebot = { API_VERSION: string }; export type Typebot = { API_VERSION: string; KEEP_OPEN: boolean };
export type Production = boolean; export type Production = boolean;
export interface Env { export interface Env {
@ -169,8 +177,8 @@ export class ConfigService {
this.env = !(process.env?.DOCKER_ENV === 'true') ? this.envYaml() : this.envProcess(); this.env = !(process.env?.DOCKER_ENV === 'true') ? this.envYaml() : this.envProcess();
this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD'; this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD';
if (process.env?.DOCKER_ENV === 'true') { if (process.env?.DOCKER_ENV === 'true') {
this.env.SERVER.TYPE = 'http'; this.env.SERVER.TYPE = process.env.SERVER_TYPE as 'http' | 'http';
this.env.SERVER.PORT = 8080; this.env.SERVER.PORT = Number.parseInt(process.env.SERVER_PORT) || 8080;
} }
} }
@ -181,9 +189,11 @@ export class ConfigService {
private envProcess(): Env { private envProcess(): Env {
return { return {
SERVER: { SERVER: {
TYPE: process.env.SERVER_TYPE as 'http' | 'https', TYPE: (process.env.SERVER_TYPE as 'http' | 'https') || 'http',
PORT: Number.parseInt(process.env.SERVER_PORT) || 8080, PORT: Number.parseInt(process.env.SERVER_PORT) || 8080,
URL: process.env.SERVER_URL, URL: process.env.SERVER_URL,
DISABLE_DOCS: process.env?.SERVER_DISABLE_DOCS === 'true',
DISABLE_MANAGER: process.env?.SERVER_DISABLE_MANAGER === 'true',
}, },
CORS: { CORS: {
ORIGIN: process.env.CORS_ORIGIN.split(',') || ['*'], ORIGIN: process.env.CORS_ORIGIN.split(',') || ['*'],
@ -267,6 +277,8 @@ export class ConfigService {
}, },
EVENTS: { EVENTS: {
APPLICATION_STARTUP: process.env?.WEBHOOK_EVENTS_APPLICATION_STARTUP === 'true', APPLICATION_STARTUP: process.env?.WEBHOOK_EVENTS_APPLICATION_STARTUP === 'true',
INSTANCE_CREATE: process.env?.WEBHOOK_EVENTS_INSTANCE_CREATE === 'true',
INSTANCE_DELETE: process.env?.WEBHOOK_EVENTS_INSTANCE_DELETE === 'true',
QRCODE_UPDATED: process.env?.WEBHOOK_EVENTS_QRCODE_UPDATED === 'true', QRCODE_UPDATED: process.env?.WEBHOOK_EVENTS_QRCODE_UPDATED === 'true',
MESSAGES_SET: process.env?.WEBHOOK_EVENTS_MESSAGES_SET === 'true', MESSAGES_SET: process.env?.WEBHOOK_EVENTS_MESSAGES_SET === 'true',
MESSAGES_UPSERT: process.env?.WEBHOOK_EVENTS_MESSAGES_UPSERT === 'true', MESSAGES_UPSERT: process.env?.WEBHOOK_EVENTS_MESSAGES_UPSERT === 'true',
@ -304,6 +316,7 @@ export class ConfigService {
}, },
TYPEBOT: { TYPEBOT: {
API_VERSION: process.env?.TYPEBOT_API_VERSION || 'old', API_VERSION: process.env?.TYPEBOT_API_VERSION || 'old',
KEEP_OPEN: process.env.TYPEBOT_KEEP_OPEN === 'true',
}, },
AUTHENTICATION: { AUTHENTICATION: {
TYPE: process.env.AUTHENTICATION_TYPE as 'apikey', TYPE: process.env.AUTHENTICATION_TYPE as 'apikey',

View File

@ -9,6 +9,9 @@ SERVER:
TYPE: http # https TYPE: http # https
PORT: 8080 # 443 PORT: 8080 # 443
URL: localhost URL: localhost
DISABLE_MANAGER: false
DISABLE_DOCS: false
CORS: CORS:
ORIGIN: ORIGIN:
@ -148,6 +151,7 @@ QRCODE:
TYPEBOT: TYPEBOT:
API_VERSION: 'old' # old | latest API_VERSION: 'old' # old | latest
KEEP_OPEN: false
# Defines an authentication type for the api # Defines an authentication type for the api
# We recommend using the apikey because it will allow you to use a custom token, # We recommend using the apikey because it will allow you to use a custom token,

View File

@ -25,7 +25,7 @@ info:
</font> </font>
[![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/26869335-5546d063-156b-4529-915f-909dd628c090?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D26869335-5546d063-156b-4529-915f-909dd628c090%26entityType%3Dcollection%26workspaceId%3D339a4ee7-378b-45c9-b5b8-fd2c0a9c2442) [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/26869335-5546d063-156b-4529-915f-909dd628c090?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D26869335-5546d063-156b-4529-915f-909dd628c090%26entityType%3Dcollection%26workspaceId%3D339a4ee7-378b-45c9-b5b8-fd2c0a9c2442)
version: 1.5.5 version: 1.6.1
contact: contact:
name: DavidsonGomes name: DavidsonGomes
email: contato@agenciadgcode.com email: contato@agenciadgcode.com

View File

@ -53,7 +53,8 @@ function bootstrap() {
app.use('/store', express.static(join(ROOT_DIR, 'store'))); app.use('/store', express.static(join(ROOT_DIR, 'store')));
app.use('/', router); app.use('/', router);
app.use(swaggerRouter);
if (!configService.get('SERVER').DISABLE_DOCS) app.use(swaggerRouter);
app.use( app.use(
(err: Error, req: Request, res: Response, next: NextFunction) => { (err: Error, req: Request, res: Response, next: NextFunction) => {

View File

@ -889,6 +889,7 @@ export const chatwootSchema: JSONSchema7 = {
token: { type: 'string' }, token: { type: 'string' },
url: { type: 'string' }, url: { type: 'string' },
sign_msg: { type: 'boolean', enum: [true, false] }, sign_msg: { type: 'boolean', enum: [true, false] },
sign_delimiter: { type: ['string', 'null'] },
reopen_conversation: { type: 'boolean', enum: [true, false] }, reopen_conversation: { type: 'boolean', enum: [true, false] },
conversation_pending: { type: 'boolean', enum: [true, false] }, conversation_pending: { type: 'boolean', enum: [true, false] },
auto_create: { type: 'boolean', enum: [true, false] }, auto_create: { type: 'boolean', enum: [true, false] },

View File

@ -37,6 +37,7 @@ export class ChatwootController {
if (data.sign_msg !== true && data.sign_msg !== false) { if (data.sign_msg !== true && data.sign_msg !== false) {
throw new BadRequestException('sign_msg is required'); throw new BadRequestException('sign_msg is required');
} }
if (data.sign_msg === false) data.sign_delimiter = null;
} }
if (!data.enabled) { if (!data.enabled) {
@ -45,6 +46,7 @@ export class ChatwootController {
data.token = ''; data.token = '';
data.url = ''; data.url = '';
data.sign_msg = false; data.sign_msg = false;
data.sign_delimiter = null;
data.reopen_conversation = false; data.reopen_conversation = false;
data.conversation_pending = false; data.conversation_pending = false;
data.auto_create = false; data.auto_create = false;

View File

@ -1,6 +1,7 @@
import { delay } from '@whiskeysockets/baileys'; import { delay } from '@whiskeysockets/baileys';
import { isURL } from 'class-validator'; import { isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import { v4 } from 'uuid';
import { ConfigService, HttpServer } from '../../config/env.config'; import { ConfigService, HttpServer } from '../../config/env.config';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
@ -19,7 +20,7 @@ import { TypebotService } from '../services/typebot.service';
import { WebhookService } from '../services/webhook.service'; import { WebhookService } from '../services/webhook.service';
import { WebsocketService } from '../services/websocket.service'; import { WebsocketService } from '../services/websocket.service';
import { WAStartupService } from '../services/whatsapp.service'; import { WAStartupService } from '../services/whatsapp.service';
import { wa } from '../types/wa.types'; import { Events, wa } from '../types/wa.types';
export class InstanceController { export class InstanceController {
constructor( constructor(
@ -87,6 +88,13 @@ export class InstanceController {
const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache);
instance.instanceName = instanceName; instance.instanceName = instanceName;
const instanceId = v4();
instance.sendDataWebhook(Events.INSTANCE_CREATE, {
instanceName,
instanceId: instanceId,
});
this.logger.verbose('instance: ' + instance.instanceName + ' created'); this.logger.verbose('instance: ' + instance.instanceName + ' created');
this.waMonitor.waInstances[instance.instanceName] = instance; this.waMonitor.waInstances[instance.instanceName] = instance;
@ -96,6 +104,7 @@ export class InstanceController {
const hash = await this.authService.generateHash( const hash = await this.authService.generateHash(
{ {
instanceName: instance.instanceName, instanceName: instance.instanceName,
instanceId: instanceId,
}, },
token, token,
); );
@ -363,6 +372,7 @@ export class InstanceController {
const result = { const result = {
instance: { instance: {
instanceName: instance.instanceName, instanceName: instance.instanceName,
instanceId: instanceId,
status: 'created', status: 'created',
}, },
hash, hash,
@ -455,6 +465,7 @@ export class InstanceController {
return { return {
instance: { instance: {
instanceName: instance.instanceName, instanceName: instance.instanceName,
instanceId: instanceId,
status: 'created', status: 'created',
}, },
hash, hash,
@ -580,11 +591,13 @@ export class InstanceController {
}; };
} }
public async fetchInstances({ instanceName }: InstanceDto) { public async fetchInstances({ instanceName, instanceId }: InstanceDto) {
if (instanceName) { if (instanceName) {
this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance'); this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance');
this.logger.verbose('instanceName: ' + instanceName); this.logger.verbose('instanceName: ' + instanceName);
return this.waMonitor.instanceInfo(instanceName); return this.waMonitor.instanceInfo(instanceName);
} else if (instanceId) {
return this.waMonitor.instanceInfoById(instanceId);
} }
this.logger.verbose('requested fetchInstances (all instances)'); this.logger.verbose('requested fetchInstances (all instances)');
@ -630,6 +643,10 @@ export class InstanceController {
this.logger.verbose('deleting instance: ' + instanceName); this.logger.verbose('deleting instance: ' + instanceName);
this.waMonitor.waInstances[instanceName].sendDataWebhook(Events.INSTANCE_DELETE, {
instanceName,
instanceId: (await this.repository.auth.find(instanceName))?.instanceId,
});
delete this.waMonitor.waInstances[instanceName]; delete this.waMonitor.waInstances[instanceName];
this.eventEmitter.emit('remove.instance', instanceName, 'inner'); this.eventEmitter.emit('remove.instance', instanceName, 'inner');
return { status: 'SUCCESS', error: false, response: { message: 'Instance deleted' } }; return { status: 'SUCCESS', error: false, response: { message: 'Instance deleted' } };

View File

@ -5,6 +5,7 @@ export class ChatwootDto {
url?: string; url?: string;
name_inbox?: string; name_inbox?: string;
sign_msg?: boolean; sign_msg?: boolean;
sign_delimiter?: string;
number?: string; number?: string;
reopen_conversation?: boolean; reopen_conversation?: boolean;
conversation_pending?: boolean; conversation_pending?: boolean;

View File

@ -1,5 +1,6 @@
export class InstanceDto { export class InstanceDto {
instanceName: string; instanceName: string;
instanceId?: string;
qrcode?: boolean; qrcode?: boolean;
number?: string; number?: string;
token?: string; token?: string;

View File

@ -6,12 +6,14 @@ export class AuthRaw {
_id?: string; _id?: string;
jwt?: string; jwt?: string;
apikey?: string; apikey?: string;
instanceId?: string;
} }
const authSchema = new Schema<AuthRaw>({ const authSchema = new Schema<AuthRaw>({
_id: { type: String, _id: true }, _id: { type: String, _id: true },
jwt: { type: String, minlength: 1 }, jwt: { type: String, minlength: 1 },
apikey: { type: String, minlength: 1 }, apikey: { type: String, minlength: 1 },
instanceId: { type: String, minlength: 1 },
}); });
export const AuthModel = dbserver?.model(AuthRaw.name, authSchema, 'authentication'); export const AuthModel = dbserver?.model(AuthRaw.name, authSchema, 'authentication');

View File

@ -10,6 +10,7 @@ export class ChatwootRaw {
url?: string; url?: string;
name_inbox?: string; name_inbox?: string;
sign_msg?: boolean; sign_msg?: boolean;
sign_delimiter?: string;
number?: string; number?: string;
reopen_conversation?: boolean; reopen_conversation?: boolean;
conversation_pending?: boolean; conversation_pending?: boolean;
@ -23,6 +24,7 @@ const chatwootSchema = new Schema<ChatwootRaw>({
url: { type: String, required: true }, url: { type: String, required: true },
name_inbox: { type: String, required: true }, name_inbox: { type: String, required: true },
sign_msg: { type: Boolean, required: true }, sign_msg: { type: Boolean, required: true },
sign_delimiter: { type: String, required: false },
number: { type: String, required: true }, number: { type: String, required: true },
reopen_conversation: { type: Boolean, required: true }, reopen_conversation: { type: Boolean, required: true },
conversation_pending: { type: Boolean, required: true }, conversation_pending: { type: Boolean, required: true },

View File

@ -10,6 +10,12 @@ class Key {
participant?: string; participant?: string;
} }
class ChatwootMessage {
messageId?: number;
inboxId?: number;
conversationId?: number;
}
export class MessageRaw { export class MessageRaw {
_id?: string; _id?: string;
key?: Key; key?: Key;
@ -22,7 +28,7 @@ export class MessageRaw {
source?: 'android' | 'web' | 'ios'; source?: 'android' | 'web' | 'ios';
source_id?: string; source_id?: string;
source_reply_id?: string; source_reply_id?: string;
chatwootMessageId?: string; chatwoot?: ChatwootMessage;
} }
const messageSchema = new Schema<MessageRaw>({ const messageSchema = new Schema<MessageRaw>({
@ -40,10 +46,14 @@ const messageSchema = new Schema<MessageRaw>({
source: { type: String, minlength: 3, enum: ['android', 'web', 'ios'] }, source: { type: String, minlength: 3, enum: ['android', 'web', 'ios'] },
messageTimestamp: { type: Number, required: true }, messageTimestamp: { type: Number, required: true },
owner: { type: String, required: true, minlength: 1 }, owner: { type: String, required: true, minlength: 1 },
chatwootMessageId: { type: String, required: false }, chatwoot: {
messageId: { type: Number },
inboxId: { type: Number },
conversationId: { type: Number },
},
}); });
messageSchema.index({ chatwootMessageId: 1, owner: 1 }); messageSchema.index({ 'chatwoot.messageId': 1, owner: 1 });
messageSchema.index({ 'key.id': 1 }); messageSchema.index({ 'key.id': 1 });
messageSchema.index({ 'key.id': 1, owner: 1 }); messageSchema.index({ 'key.id': 1, owner: 1 });
messageSchema.index({ owner: 1 }); messageSchema.index({ owner: 1 });

View File

@ -19,6 +19,7 @@ export class AuthRepository extends Repository {
public async create(data: AuthRaw, instance: string): Promise<IInsert> { public async create(data: AuthRaw, instance: string): Promise<IInsert> {
try { try {
this.logger.verbose('creating auth'); this.logger.verbose('creating auth');
if (this.dbSettings.ENABLED) { if (this.dbSettings.ENABLED) {
this.logger.verbose('saving auth to db'); this.logger.verbose('saving auth to db');
const insert = await this.authModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); const insert = await this.authModel.replaceOne({ _id: instance }, { ...data }, { upsert: true });
@ -62,4 +63,20 @@ export class AuthRepository extends Repository {
return {}; return {};
} }
} }
public async findInstanceNameById(instanceId: string): Promise<string | null> {
try {
this.logger.verbose('finding auth by instanceId');
if (this.dbSettings.ENABLED) {
this.logger.verbose('finding auth in db');
const response = await this.authModel.findOne({ instanceId });
return response._id;
}
this.logger.verbose('finding auth in store is not supported');
} catch (error) {
return null;
}
}
} }

View File

@ -91,11 +91,13 @@ export class MessageRepository extends Repository {
this.logger.verbose('finding messages'); this.logger.verbose('finding messages');
if (this.dbSettings.ENABLED) { if (this.dbSettings.ENABLED) {
this.logger.verbose('finding messages in db'); this.logger.verbose('finding messages in db');
if (query?.where?.key) { for (const [o, p] of Object.entries(query?.where)) {
for (const [k, v] of Object.entries(query.where.key)) { if (typeof p === 'object' && p !== null && !Array.isArray(p)) {
query.where['key.' + k] = v; for (const [k, v] of Object.entries(p)) {
query.where[`${o}.${k}`] = v;
}
delete query.where[o];
} }
delete query?.where?.key;
} }
return await this.messageModel return await this.messageModel

View File

@ -31,22 +31,25 @@ enum HttpStatus {
const router = Router(); const router = Router();
const authType = configService.get<Auth>('AUTHENTICATION').TYPE; const authType = configService.get<Auth>('AUTHENTICATION').TYPE;
const serverConfig = configService.get('SERVER');
const guards = [instanceExistsGuard, instanceLoggedGuard, authGuard[authType]]; const guards = [instanceExistsGuard, instanceLoggedGuard, authGuard[authType]];
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
if (!serverConfig.DISABLE_MANAGER) router.use('/manager', new ViewsRouter().router);
router router
.get('/', (req, res) => { .get('/', (req, res) => {
res.status(HttpStatus.OK).json({ res.status(HttpStatus.OK).json({
status: HttpStatus.OK, status: HttpStatus.OK,
message: 'Welcome to the Evolution API, it is working!', message: 'Welcome to the Evolution API, it is working!',
version: packageJson.version, version: packageJson.version,
documentation: `${req.protocol}://${req.get('host')}/docs`, swagger: !serverConfig.DISABLE_DOCS ? `${req.protocol}://${req.get('host')}/docs` : undefined,
manager: `${req.protocol}://${req.get('host')}/manager`, manager: !serverConfig.DISABLE_MANAGER ? `${req.protocol}://${req.get('host')}/manager` : undefined,
documentation: `https://doc.evolution-api.com`,
}); });
}) })
.use('/instance', new InstanceRouter(configService, ...guards).router) .use('/instance', new InstanceRouter(configService, ...guards).router)
.use('/manager', new ViewsRouter().router)
.use('/message', new MessageRouter(...guards).router) .use('/message', new MessageRouter(...guards).router)
.use('/chat', new ChatRouter(...guards).router) .use('/chat', new ChatRouter(...guards).router)
.use('/group', new GroupRouter(...guards).router) .use('/group', new GroupRouter(...guards).router)

View File

@ -46,7 +46,10 @@ export class AuthService {
this.logger.verbose('JWT token created: ' + token); this.logger.verbose('JWT token created: ' + token);
const auth = await this.repository.auth.create({ jwt: token }, instance.instanceName); const auth = await this.repository.auth.create(
{ jwt: token, instanceId: instance.instanceId },
instance.instanceName,
);
this.logger.verbose('JWT token saved in database'); this.logger.verbose('JWT token saved in database');
@ -66,7 +69,7 @@ export class AuthService {
this.logger.verbose(token ? 'APIKEY defined: ' + apikey : 'APIKEY created: ' + apikey); this.logger.verbose(token ? 'APIKEY defined: ' + apikey : 'APIKEY created: ' + apikey);
const auth = await this.repository.auth.create({ apikey }, instance.instanceName); const auth = await this.repository.auth.create({ apikey, instanceId: instance.instanceId }, instance.instanceName);
this.logger.verbose('APIKEY saved in database'); this.logger.verbose('APIKEY saved in database');

View File

@ -14,6 +14,7 @@ import { InstanceDto } from '../dto/instance.dto';
import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto';
import { MessageRaw } from '../models'; import { MessageRaw } from '../models';
import { RepositoryBroker } from '../repository/repository.manager'; import { RepositoryBroker } from '../repository/repository.manager';
import { Events } from '../types/wa.types';
import { WAMonitoringService } from './monitor.service'; import { WAMonitoringService } from './monitor.service';
export class ChatwootService { export class ChatwootService {
@ -920,7 +921,11 @@ export class ChatwootService {
const fileName = decodeURIComponent(parts[parts.length - 1]); const fileName = decodeURIComponent(parts[parts.length - 1]);
this.logger.verbose('file name: ' + fileName); this.logger.verbose('file name: ' + fileName);
const mimeType = mimeTypes.lookup(fileName).toString(); const response = await axios.get(media, {
responseType: 'arraybuffer',
});
const mimeType = response.headers['content-type'];
this.logger.verbose('mime type: ' + mimeType); this.logger.verbose('mime type: ' + mimeType);
let type = 'document'; let type = 'document';
@ -1015,8 +1020,16 @@ export class ChatwootService {
this.logger.verbose('check if is group'); this.logger.verbose('check if is group');
const chatId = const chatId =
body.conversation.meta.sender?.phone_number?.replace('+', '') || body.conversation.meta.sender?.identifier; body.conversation.meta.sender?.phone_number?.replace('+', '') || body.conversation.meta.sender?.identifier;
const messageReceived = body.content; // Chatwoot to Whatsapp
const senderName = body?.sender?.name; const messageReceived = body.content
? body.content
.replaceAll(/(?<!\*)\*((?!\s)([^\n*]+?)(?<!\s))\*(?!\*)/g, '_$1_') // Substitui * por _
.replaceAll(/\*{2}((?!\s)([^\n*]+?)(?<!\s))\*{2}/g, '*$1*') // Substitui ** por *
.replaceAll(/~{2}((?!\s)([^\n*]+?)(?<!\s))~{2}/g, '~$1~') // Substitui ~~ por ~
.replaceAll(/(?<!`)`((?!\s)([^`*]+?)(?<!\s))`(?!`)/g, '```$1```') // Substitui ` por ```
: body.content;
const senderName = body?.sender?.available_name || body?.sender?.name;
const waInstance = this.waMonitor.waInstances[instance.instanceName]; const waInstance = this.waMonitor.waInstances[instance.instanceName];
this.logger.verbose('check if is a message deletion'); this.logger.verbose('check if is a message deletion');
@ -1024,7 +1037,9 @@ export class ChatwootService {
const message = await this.repository.message.find({ const message = await this.repository.message.find({
where: { where: {
owner: instance.instanceName, owner: instance.instanceName,
chatwootMessageId: body.id, chatwoot: {
messageId: body.id,
},
}, },
limit: 1, limit: 1,
}); });
@ -1111,7 +1126,13 @@ export class ChatwootService {
if (senderName === null || senderName === undefined) { if (senderName === null || senderName === undefined) {
formatText = messageReceived; formatText = messageReceived;
} else { } else {
formatText = this.provider.sign_msg ? `*${senderName}:*\n${messageReceived}` : messageReceived; const formattedDelimiter = this.provider.sign_delimiter
? this.provider.sign_delimiter.replaceAll('\\n', '\n')
: '\n';
const textToConcat = this.provider.sign_msg ? [`*${senderName}:*`] : [];
textToConcat.push(messageReceived);
formatText = textToConcat.join(formattedDelimiter);
} }
for (const message of body.conversation.messages) { for (const message of body.conversation.messages) {
@ -1142,7 +1163,11 @@ export class ChatwootService {
...messageSent, ...messageSent,
owner: instance.instanceName, owner: instance.instanceName,
}, },
body.id, {
messageId: body.id,
inboxId: body.inbox?.id,
conversationId: body.conversation?.id,
},
instance, instance,
); );
} }
@ -1169,7 +1194,11 @@ export class ChatwootService {
...messageSent, ...messageSent,
owner: instance.instanceName, owner: instance.instanceName,
}, },
body.id, {
messageId: body.id,
inboxId: body.inbox?.id,
conversationId: body.conversation?.id,
},
instance, instance,
); );
} }
@ -1203,14 +1232,33 @@ export class ChatwootService {
} }
} }
private updateChatwootMessageId(message: MessageRaw, chatwootMessageId: string, instance: InstanceDto) { private updateChatwootMessageId(
if (!chatwootMessageId) { message: MessageRaw,
chatwootMessageIds: MessageRaw['chatwoot'],
instance: InstanceDto,
) {
if (!chatwootMessageIds.messageId || !message?.key?.id) {
return; return;
} }
message.chatwootMessageId = chatwootMessageId;
message.chatwoot = chatwootMessageIds;
this.repository.message.update([message], instance.instanceName, true); this.repository.message.update([message], instance.instanceName, true);
} }
private async getMessageByKeyId(instance: InstanceDto, keyId: string): Promise<MessageRaw> {
const messages = await this.repository.message.find({
where: {
key: {
id: keyId,
},
owner: instance.instanceName,
},
limit: 1,
});
return messages.length ? messages[0] : null;
}
private async getReplyToIds( private async getReplyToIds(
msg: any, msg: any,
instance: InstanceDto, instance: InstanceDto,
@ -1221,17 +1269,9 @@ export class ChatwootService {
if (msg) { if (msg) {
inReplyToExternalId = msg.message?.extendedTextMessage?.contextInfo?.stanzaId; inReplyToExternalId = msg.message?.extendedTextMessage?.contextInfo?.stanzaId;
if (inReplyToExternalId) { if (inReplyToExternalId) {
const message = await this.repository.message.find({ const message = await this.getMessageByKeyId(instance, inReplyToExternalId);
where: { if (message?.chatwoot?.messageId) {
key: { inReplyTo = message.chatwoot.messageId;
id: inReplyToExternalId,
},
owner: instance.instanceName,
},
limit: 1,
});
if (message.length && message[0]?.chatwootMessageId) {
inReplyTo = message[0].chatwootMessageId;
} }
} }
} }
@ -1246,7 +1286,9 @@ export class ChatwootService {
if (msg?.content_attributes?.in_reply_to) { if (msg?.content_attributes?.in_reply_to) {
const message = await this.repository.message.find({ const message = await this.repository.message.find({
where: { where: {
chatwootMessageId: msg?.content_attributes?.in_reply_to, chatwoot: {
messageId: msg?.content_attributes?.in_reply_to,
},
owner: instance.instanceName, owner: instance.instanceName,
}, },
limit: 1, limit: 1,
@ -1466,7 +1508,17 @@ export class ChatwootService {
} }
this.logger.verbose('get conversation message'); this.logger.verbose('get conversation message');
const bodyMessage = await this.getConversationMessage(body.message);
// Whatsapp to Chatwoot
const originalMessage = await this.getConversationMessage(body.message);
const bodyMessage = originalMessage
? originalMessage
.replaceAll(/\*((?!\s)([^\n*]+?)(?<!\s))\*/g, '**$1**')
.replaceAll(/_((?!\s)([^\n_]+?)(?<!\s))_/g, '*$1*')
.replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~')
: originalMessage;
this.logger.verbose('body message: ' + bodyMessage);
if (bodyMessage && bodyMessage.includes('Por favor, classifique esta conversa, http')) { if (bodyMessage && bodyMessage.includes('Por favor, classifique esta conversa, http')) {
this.logger.verbose('conversation is closed'); this.logger.verbose('conversation is closed');
@ -1728,6 +1780,25 @@ export class ChatwootService {
} }
} }
if (event === Events.MESSAGES_DELETE) {
this.logger.verbose('deleting message from instance: ' + instance.instanceName);
if (!body?.key?.id) {
this.logger.warn('message id not found');
return;
}
const message = await this.getMessageByKeyId(instance, body.key.id);
if (message?.chatwoot?.messageId && message?.chatwoot?.conversationId) {
this.logger.verbose('deleting message in chatwoot. Message id: ' + body.key.id);
return await client.messages.delete({
accountId: this.provider.account_id,
conversationId: message.chatwoot.conversationId,
messageId: message.chatwoot.messageId,
});
}
}
if (event === 'status.instance') { if (event === 'status.instance') {
this.logger.verbose('event status.instance'); this.logger.verbose('event status.instance');
const data = body; const data = body;

View File

@ -112,6 +112,7 @@ export class WAMonitoringService {
const instanceData = { const instanceData = {
instance: { instance: {
instanceName: key, instanceName: key,
instanceId: (await this.repository.auth.find(key))?.instanceId,
owner: value.wuid, owner: value.wuid,
profileName: (await value.getProfileName()) || 'not loaded', profileName: (await value.getProfileName()) || 'not loaded',
profilePictureUrl: value.profilePictureUrl, profilePictureUrl: value.profilePictureUrl,
@ -135,6 +136,89 @@ export class WAMonitoringService {
const instanceData = { const instanceData = {
instance: { instance: {
instanceName: key, instanceName: key,
instanceId: (await this.repository.auth.find(key))?.instanceId,
status: value.connectionStatus.state,
},
};
if (this.configService.get<Auth>('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) {
instanceData.instance['serverUrl'] = this.configService.get<HttpServer>('SERVER').URL;
instanceData.instance['apikey'] = (await this.repository.auth.find(key))?.apikey;
instanceData.instance['chatwoot'] = chatwoot;
}
instances.push(instanceData);
}
}
}
this.logger.verbose('return instance info: ' + instances.length);
return instances.find((i) => i.instance.instanceName === instanceName) ?? instances;
}
public async instanceInfoById(instanceId?: string) {
this.logger.verbose('get instance info');
const instanceName = await this.repository.auth.findInstanceNameById(instanceId);
if (!instanceName) {
throw new NotFoundException(`Instance "${instanceId}" not found`);
}
if (instanceName && !this.waInstances[instanceName]) {
throw new NotFoundException(`Instance "${instanceName}" not found`);
}
const instances: any[] = [];
for await (const [key, value] of Object.entries(this.waInstances)) {
if (value) {
this.logger.verbose('get instance info: ' + key);
let chatwoot: any;
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
const findChatwoot = await this.waInstances[key].findChatwoot();
if (findChatwoot && findChatwoot.enabled) {
chatwoot = {
...findChatwoot,
webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(key)}`,
};
}
if (value.connectionStatus.state === 'open') {
this.logger.verbose('instance: ' + key + ' - connectionStatus: open');
const instanceData = {
instance: {
instanceName: key,
instanceId: (await this.repository.auth.find(key))?.instanceId,
owner: value.wuid,
profileName: (await value.getProfileName()) || 'not loaded',
profilePictureUrl: value.profilePictureUrl,
profileStatus: (await value.getProfileStatus()) || '',
status: value.connectionStatus.state,
},
};
if (this.configService.get<Auth>('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) {
instanceData.instance['serverUrl'] = this.configService.get<HttpServer>('SERVER').URL;
instanceData.instance['apikey'] = (await this.repository.auth.find(key))?.apikey;
instanceData.instance['chatwoot'] = chatwoot;
}
instances.push(instanceData);
} else {
this.logger.verbose('instance: ' + key + ' - connectionStatus: ' + value.connectionStatus.state);
const instanceData = {
instance: {
instanceName: key,
instanceId: (await this.repository.auth.find(key))?.instanceId,
status: value.connectionStatus.state, status: value.connectionStatus.state,
}, },
}; };
@ -193,12 +277,6 @@ export class WAMonitoringService {
public async cleaningUp(instanceName: string) { public async cleaningUp(instanceName: string) {
this.logger.verbose('cleaning up instance: ' + instanceName); this.logger.verbose('cleaning up instance: ' + instanceName);
if (this.redis.ENABLED) {
this.logger.verbose('cleaning up instance in redis: ' + instanceName);
this.cache.reference = instanceName;
await this.cache.delAll();
return;
}
if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) {
this.logger.verbose('cleaning up instance in database: ' + instanceName); this.logger.verbose('cleaning up instance in database: ' + instanceName);
@ -210,6 +288,13 @@ export class WAMonitoringService {
return; return;
} }
if (this.redis.ENABLED) {
this.logger.verbose('cleaning up instance in redis: ' + instanceName);
this.cache.reference = instanceName;
await this.cache.delAll();
return;
}
this.logger.verbose('cleaning up instance in files: ' + instanceName); this.logger.verbose('cleaning up instance in files: ' + instanceName);
rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true });
} }
@ -357,8 +442,8 @@ export class WAMonitoringService {
this.eventEmitter.on('logout.instance', async (instanceName: string) => { this.eventEmitter.on('logout.instance', async (instanceName: string) => {
this.logger.verbose('logout instance: ' + instanceName); this.logger.verbose('logout instance: ' + instanceName);
try { try {
// this.logger.verbose('request cleaning up instance: ' + instanceName); this.logger.verbose('request cleaning up instance: ' + instanceName);
// this.cleaningUp(instanceName); this.cleaningUp(instanceName);
} finally { } finally {
this.logger.warn(`Instance "${instanceName}" - LOGOUT`); this.logger.warn(`Instance "${instanceName}" - LOGOUT`);
} }

View File

@ -1,4 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import EventEmitter2 from 'eventemitter2';
import { ConfigService, Typebot } from '../../config/env.config'; import { ConfigService, Typebot } from '../../config/env.config';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
@ -9,7 +10,18 @@ import { Events } from '../types/wa.types';
import { WAMonitoringService } from './monitor.service'; import { WAMonitoringService } from './monitor.service';
export class TypebotService { export class TypebotService {
constructor(private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService) {} constructor(
private readonly waMonitor: WAMonitoringService,
private readonly configService: ConfigService,
private readonly eventEmitter: EventEmitter2,
) {
this.eventEmitter.on('typebot:end', async (data) => {
const keep_open = this.configService.get<Typebot>('TYPEBOT').KEEP_OPEN;
if (keep_open) return;
await this.clearSessions(data.instance, data.remoteJid);
});
}
private readonly logger = new Logger(TypebotService.name); private readonly logger = new Logger(TypebotService.name);
@ -110,6 +122,37 @@ export class TypebotService {
return { typebot: { ...instance, typebot: typebotData } }; return { typebot: { ...instance, typebot: typebotData } };
} }
public async clearSessions(instance: InstanceDto, remoteJid: string) {
const findTypebot = await this.find(instance);
const sessions = (findTypebot.sessions as Session[]) ?? [];
const sessionWithRemoteJid = sessions.filter((session) => session.remoteJid === remoteJid);
if (sessionWithRemoteJid.length > 0) {
sessionWithRemoteJid.forEach((session) => {
sessions.splice(sessions.indexOf(session), 1);
});
const typebotData = {
enabled: findTypebot.enabled,
url: findTypebot.url,
typebot: findTypebot.typebot,
expire: findTypebot.expire,
keyword_finish: findTypebot.keyword_finish,
delay_message: findTypebot.delay_message,
unknown_message: findTypebot.unknown_message,
listening_from_me: findTypebot.listening_from_me,
sessions,
};
this.create(instance, typebotData);
return sessions;
}
return sessions;
}
public async startTypebot(instance: InstanceDto, data: any) { public async startTypebot(instance: InstanceDto, data: any) {
if (data.remoteJid === 'status@broadcast') return; if (data.remoteJid === 'status@broadcast') return;
@ -169,20 +212,25 @@ export class TypebotService {
} else { } else {
const id = Math.floor(Math.random() * 10000000000).toString(); const id = Math.floor(Math.random() * 10000000000).toString();
const reqData = {
startParams: {
publicId: data.typebot,
prefilledVariables: prefilledVariables,
},
};
try { try {
const version = this.configService.get<Typebot>('TYPEBOT').API_VERSION; const version = this.configService.get<Typebot>('TYPEBOT').API_VERSION;
let url: string; let url: string;
let reqData: {};
if (version === 'latest') { if (version === 'latest') {
url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`; url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`;
reqData = {
prefilledVariables: prefilledVariables,
};
} else { } else {
url = `${data.url}/api/v1/sendMessage`; url = `${data.url}/api/v1/sendMessage`;
reqData = {
startParams: {
publicId: data.typebot,
prefilledVariables: prefilledVariables,
},
};
} }
const request = await axios.post(url, reqData); const request = await axios.post(url, reqData);
@ -260,25 +308,35 @@ export class TypebotService {
if (data.remoteJid === 'status@broadcast') return; if (data.remoteJid === 'status@broadcast') return;
const id = Math.floor(Math.random() * 10000000000).toString(); const id = Math.floor(Math.random() * 10000000000).toString();
const reqData = {
startParams: {
publicId: data.typebot,
prefilledVariables: {
...data.prefilledVariables,
remoteJid: data.remoteJid,
pushName: data.pushName || data.prefilledVariables?.pushName || '',
instanceName: instance.instanceName,
},
},
};
try { try {
const version = this.configService.get<Typebot>('TYPEBOT').API_VERSION; const version = this.configService.get<Typebot>('TYPEBOT').API_VERSION;
let url: string; let url: string;
let reqData: {};
if (version === 'latest') { if (version === 'latest') {
url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`; url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`;
reqData = {
prefilledVariables: {
...data.prefilledVariables,
remoteJid: data.remoteJid,
pushName: data.pushName || data.prefilledVariables?.pushName || '',
instanceName: instance.instanceName,
},
};
} else { } else {
url = `${data.url}/api/v1/sendMessage`; url = `${data.url}/api/v1/sendMessage`;
reqData = {
startParams: {
publicId: data.typebot,
prefilledVariables: {
...data.prefilledVariables,
remoteJid: data.remoteJid,
pushName: data.pushName || data.prefilledVariables?.pushName || '',
instanceName: instance.instanceName,
},
},
};
} }
const request = await axios.post(url, reqData); const request = await axios.post(url, reqData);
@ -318,37 +376,6 @@ export class TypebotService {
} }
} }
public async clearSessions(instance: InstanceDto, remoteJid: string) {
const findTypebot = await this.find(instance);
const sessions = (findTypebot.sessions as Session[]) ?? [];
const sessionWithRemoteJid = sessions.filter((session) => session.remoteJid === remoteJid);
if (sessionWithRemoteJid.length > 0) {
sessionWithRemoteJid.forEach((session) => {
sessions.splice(sessions.indexOf(session), 1);
});
const typebotData = {
enabled: findTypebot.enabled,
url: findTypebot.url,
typebot: findTypebot.typebot,
expire: findTypebot.expire,
keyword_finish: findTypebot.keyword_finish,
delay_message: findTypebot.delay_message,
unknown_message: findTypebot.unknown_message,
listening_from_me: findTypebot.listening_from_me,
sessions,
};
this.create(instance, typebotData);
return sessions;
}
return sessions;
}
public async sendWAMessage( public async sendWAMessage(
instance: InstanceDto, instance: InstanceDto,
remoteJid: string, remoteJid: string,
@ -356,11 +383,15 @@ export class TypebotService {
input: any[], input: any[],
clientSideActions: any[], clientSideActions: any[],
) { ) {
processMessages(this.waMonitor.waInstances[instance.instanceName], messages, input, clientSideActions).catch( processMessages(
(err) => { this.waMonitor.waInstances[instance.instanceName],
console.error('Erro ao processar mensagens:', err); messages,
}, input,
); clientSideActions,
this.eventEmitter,
).catch((err) => {
console.error('Erro ao processar mensagens:', err);
});
function findItemAndGetSecondsToWait(array, targetId) { function findItemAndGetSecondsToWait(array, targetId) {
if (!array) return null; if (!array) return null;
@ -373,7 +404,7 @@ export class TypebotService {
return null; return null;
} }
async function processMessages(instance, messages, input, clientSideActions) { async function processMessages(instance, messages, input, clientSideActions, eventEmitter) {
for (const message of messages) { for (const message of messages) {
const wait = findItemAndGetSecondsToWait(clientSideActions, message.id); const wait = findItemAndGetSecondsToWait(clientSideActions, message.id);
@ -383,31 +414,50 @@ export class TypebotService {
let linkPreview = false; let linkPreview = false;
for (const richText of message.content.richText) { for (const richText of message.content.richText) {
for (const element of richText.children) { if (richText.type === 'variable') {
let text = ''; for (const child of richText.children) {
if (element.text) { for (const grandChild of child.children) {
text = element.text; formattedText += grandChild.text;
}
} }
} else {
for (const element of richText.children) {
let text = '';
if (element.bold) { if (element.type === 'inline-variable') {
text = `*${text}*`; for (const child of element.children) {
for (const grandChild of child.children) {
text += grandChild.text;
}
}
} else if (element.text) {
text = element.text;
}
// if (element.text) {
// text = element.text;
// }
if (element.bold) {
text = `*${text}*`;
}
if (element.italic) {
text = `_${text}_`;
}
if (element.underline) {
text = `*${text}*`;
}
if (element.url) {
const linkText = element.children[0].text;
text = `[${linkText}](${element.url})`;
linkPreview = true;
}
formattedText += text;
} }
if (element.italic) {
text = `_${text}_`;
}
if (element.underline) {
text = `*${text}*`;
}
if (element.url) {
const linkText = element.children[0].text;
text = `[${linkText}](${element.url})`;
linkPreview = true;
}
formattedText += text;
} }
formattedText += '\n'; formattedText += '\n';
} }
@ -494,6 +544,11 @@ export class TypebotService {
}, },
}); });
} }
} else {
eventEmitter.emit('typebot:end', {
instance: instance,
remoteJid: remoteJid,
});
} }
} }
} }
@ -582,12 +637,12 @@ export class TypebotService {
let urlTypebot: string; let urlTypebot: string;
let reqData: {}; let reqData: {};
if (version === 'latest') { if (version === 'latest') {
urlTypebot = `${data.url}/api/v1/sessions/${data.sessionId}/continueChat`; urlTypebot = `${url}/api/v1/sessions/${data.sessionId}/continueChat`;
reqData = { reqData = {
message: content, message: content,
}; };
} else { } else {
urlTypebot = `${data.url}/api/v1/sendMessage`; urlTypebot = `${url}/api/v1/sendMessage`;
reqData = { reqData = {
message: content, message: content,
sessionId: data.sessionId, sessionId: data.sessionId,
@ -679,12 +734,12 @@ export class TypebotService {
let urlTypebot: string; let urlTypebot: string;
let reqData: {}; let reqData: {};
if (version === 'latest') { if (version === 'latest') {
urlTypebot = `${data.url}/api/v1/sessions/${data.sessionId}/continueChat`; urlTypebot = `${url}/api/v1/sessions/${data.sessionId}/continueChat`;
reqData = { reqData = {
message: content, message: content,
}; };
} else { } else {
urlTypebot = `${data.url}/api/v1/sendMessage`; urlTypebot = `${url}/api/v1/sendMessage`;
reqData = { reqData = {
message: content, message: content,
sessionId: data.sessionId, sessionId: data.sessionId,

View File

@ -133,6 +133,9 @@ import { waMonitor } from '../whatsapp.module';
import { ChamaaiService } from './chamaai.service'; import { ChamaaiService } from './chamaai.service';
import { ChatwootService } from './chatwoot.service'; import { ChatwootService } from './chatwoot.service';
import { TypebotService } from './typebot.service'; import { TypebotService } from './typebot.service';
const retryCache = {};
export class WAStartupService { export class WAStartupService {
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
@ -168,7 +171,7 @@ export class WAStartupService {
private chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); private chatwootService = new ChatwootService(waMonitor, this.configService, this.repository);
private typebotService = new TypebotService(waMonitor, this.configService); private typebotService = new TypebotService(waMonitor, this.configService, this.eventEmitter);
private chamaaiService = new ChamaaiService(waMonitor, this.configService); private chamaaiService = new ChamaaiService(waMonitor, this.configService);
@ -262,6 +265,7 @@ export class WAStartupService {
pairingCode: this.instance.qrcode?.pairingCode, pairingCode: this.instance.qrcode?.pairingCode,
code: this.instance.qrcode?.code, code: this.instance.qrcode?.code,
base64: this.instance.qrcode?.base64, base64: this.instance.qrcode?.base64,
count: this.instance.qrcode?.count,
}; };
} }
@ -357,10 +361,12 @@ export class WAStartupService {
this.logger.verbose(`Chatwoot url: ${data.url}`); this.logger.verbose(`Chatwoot url: ${data.url}`);
this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`);
this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`);
this.logger.verbose(`Chatwoot sign delimiter: ${data.sign_delimiter}`);
this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`);
this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`);
Object.assign(this.localChatwoot, data); Object.assign(this.localChatwoot, { ...data, sign_delimiter: data.sign_msg ? data.sign_delimiter : null });
this.logger.verbose('Chatwoot set'); this.logger.verbose('Chatwoot set');
} }
@ -378,6 +384,7 @@ export class WAStartupService {
this.logger.verbose(`Chatwoot url: ${data.url}`); this.logger.verbose(`Chatwoot url: ${data.url}`);
this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`);
this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`);
this.logger.verbose(`Chatwoot sign delimiter: ${data.sign_delimiter}`);
this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`);
this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`);
@ -388,6 +395,7 @@ export class WAStartupService {
url: data.url, url: data.url,
name_inbox: data.name_inbox, name_inbox: data.name_inbox,
sign_msg: data.sign_msg, sign_msg: data.sign_msg,
sign_delimiter: data.sign_delimiter || null,
reopen_conversation: data.reopen_conversation, reopen_conversation: data.reopen_conversation,
conversation_pending: data.conversation_pending, conversation_pending: data.conversation_pending,
}; };
@ -1229,12 +1237,18 @@ export class WAStartupService {
this.logger.verbose('Connection opened'); this.logger.verbose('Connection opened');
this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); this.instance.wuid = this.client.user.id.replace(/:\d+/, '');
this.instance.profilePictureUrl = (await this.profilePicture(this.instance.wuid)).profilePictureUrl; this.instance.profilePictureUrl = (await this.profilePicture(this.instance.wuid)).profilePictureUrl;
const formattedWuid = this.instance.wuid.split('@')[0].padEnd(30, ' ');
const formattedName = this.instance.name;
this.logger.info( this.logger.info(
` `
CONNECTED TO WHATSAPP CONNECTED TO WHATSAPP
`.replace(/^ +/gm, ' '), `.replace(/^ +/gm, ' '),
); );
this.logger.info(`
wuid: ${formattedWuid}
name: ${formattedName}
`);
if (this.localChatwoot.enabled) { if (this.localChatwoot.enabled) {
this.chatwootService.eventWhatsapp( this.chatwootService.eventWhatsapp(
@ -1379,7 +1393,7 @@ export class WAStartupService {
}, },
logger: P({ level: this.logBaileys }), logger: P({ level: this.logBaileys }),
printQRInTerminal: false, printQRInTerminal: false,
browser, browser: number ? ['Chrome (Linux)', session.NAME, release()] : browser,
version, version,
markOnlineOnConnect: this.localSettings.always_online, markOnlineOnConnect: this.localSettings.always_online,
retryRequestDelayMs: 10, retryRequestDelayMs: 10,
@ -1466,7 +1480,7 @@ export class WAStartupService {
}, },
logger: P({ level: this.logBaileys }), logger: P({ level: this.logBaileys }),
printQRInTerminal: false, printQRInTerminal: false,
browser, browser: this.phoneNumber ? ['Chrome (Linux)', session.NAME, release()] : browser,
version, version,
markOnlineOnConnect: this.localSettings.always_online, markOnlineOnConnect: this.localSettings.always_online,
retryRequestDelayMs: 10, retryRequestDelayMs: 10,
@ -1785,7 +1799,11 @@ export class WAStartupService {
); );
if (chatwootSentMessage?.id) { if (chatwootSentMessage?.id) {
messageRaw.chatwootMessageId = chatwootSentMessage.id; messageRaw.chatwoot = {
messageId: chatwootSentMessage.id,
inboxId: chatwootSentMessage.inbox_id,
conversationId: chatwootSentMessage.conversation_id,
};
} }
} }
@ -1928,6 +1946,15 @@ export class WAStartupService {
this.instance.name, this.instance.name,
database.SAVE_DATA.MESSAGE_UPDATE, database.SAVE_DATA.MESSAGE_UPDATE,
); );
if (this.localChatwoot.enabled) {
this.chatwootService.eventWhatsapp(
Events.MESSAGES_DELETE,
{ instanceName: this.instance.name },
{ key: key },
);
}
return; return;
} }
@ -2030,12 +2057,27 @@ export class WAStartupService {
if (events['messages.upsert']) { if (events['messages.upsert']) {
this.logger.verbose('Listening event: messages.upsert'); this.logger.verbose('Listening event: messages.upsert');
const payload = events['messages.upsert']; const payload = events['messages.upsert'];
if (payload.messages.find((a) => a?.messageStubType === 2)) {
const msg = payload.messages[0];
retryCache[msg.key.id] = msg;
return;
}
this.messageHandle['messages.upsert'](payload, database, settings); this.messageHandle['messages.upsert'](payload, database, settings);
} }
if (events['messages.update']) { if (events['messages.update']) {
this.logger.verbose('Listening event: messages.update'); this.logger.verbose('Listening event: messages.update');
const payload = events['messages.update']; const payload = events['messages.update'];
payload.forEach((message) => {
if (retryCache[message.key.id]) {
this.client.ev.emit('messages.upsert', {
messages: [message],
type: 'notify',
});
delete retryCache[message.key.id];
return;
}
});
this.messageHandle['messages.update'](payload, database, settings); this.messageHandle['messages.update'](payload, database, settings);
} }
@ -2689,7 +2731,9 @@ export class WAStartupService {
mimetype = mediaMessage.mimetype; mimetype = mediaMessage.mimetype;
} else { } else {
if (isURL(mediaMessage.media)) { if (isURL(mediaMessage.media)) {
mimetype = getMIMEType(mediaMessage.media); const response = await axios.get(mediaMessage.media, { responseType: 'arraybuffer' });
mimetype = response.headers['content-type'];
} else { } else {
mimetype = getMIMEType(mediaMessage.fileName); mimetype = getMIMEType(mediaMessage.fileName);
} }

View File

@ -3,6 +3,8 @@ import { AuthenticationState, WAConnectionState } from '@whiskeysockets/baileys'
export enum Events { export enum Events {
APPLICATION_STARTUP = 'application.startup', APPLICATION_STARTUP = 'application.startup',
INSTANCE_CREATE = 'instance.create',
INSTANCE_DELETE = 'instance.delete',
QRCODE_UPDATED = 'qrcode.updated', QRCODE_UPDATED = 'qrcode.updated',
CONNECTION_UPDATE = 'connection.update', CONNECTION_UPDATE = 'connection.update',
STATUS_INSTANCE = 'status.instance', STATUS_INSTANCE = 'status.instance',

View File

@ -101,7 +101,7 @@ export const waMonitor = new WAMonitoringService(eventEmitter, configService, re
const authService = new AuthService(configService, waMonitor, repository); const authService = new AuthService(configService, waMonitor, repository);
const typebotService = new TypebotService(waMonitor, configService); const typebotService = new TypebotService(waMonitor, configService, eventEmitter);
export const typebotController = new TypebotController(typebotService); export const typebotController = new TypebotController(typebotService);