Merge branch 'develop' into main

This commit is contained in:
Davidson Gomes 2024-01-19 16:52:06 -03:00 committed by GitHub
commit 2178897d28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 3494 additions and 274 deletions

View File

@ -0,0 +1,64 @@
name: Build Docker image
on:
push:
tags: ['v*']
jobs:
build-amd:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Extract existing image metadata
id: image-meta
uses: docker/metadata-action@v4
with:
images: atendai/evolution-api
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push AMD image
uses: docker/build-push-action@v4
with:
context: .
labels: ${{ steps.image-meta.outputs.labels }}
platforms: linux/amd64
push: true
build-arm:
runs-on: buildjet-4vcpu-ubuntu-2204-arm
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Extract existing image metadata
id: image-meta
uses: docker/metadata-action@v4
with:
images: atendai/evolution-api
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push ARM image
uses: docker/build-push-action@v4
with:
context: .
labels: ${{ steps.image-meta.outputs.labels }}
platforms: linux/arm64
push: true

View File

@ -1,3 +1,22 @@
# 1.6.2 (develop)
### Feature
* Added update message endpoint
### Fixed
* Proxy configuration improvements
* Correction in sending lists
* Adjust in webhook_base64
* Correction in typebot text formatting
* Correction in chatwoot text formatting and render list message
* Only use a axios request to get file mimetype if necessary
* When possible use the original file extension
* When receiving a file from whatsapp, use the original filename in chatwoot if possible
* Remove message ids cache in chatwoot to use chatwoot's api itself
* Adjusts the quoted message, now has contextInfo in the message Raw
# 1.6.1 (2023-12-22 11:43) # 1.6.1 (2023-12-22 11:43)
### Fixed ### Fixed

View File

@ -1,6 +1,6 @@
FROM node:20.7.0-alpine AS builder FROM node:20.7.0-alpine AS builder
LABEL version="1.6.1" description="Api to control whatsapp features through http requests." LABEL version="1.6.2" 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"

View File

@ -1,6 +1,6 @@
{ {
"name": "evolution-api", "name": "evolution-api",
"version": "1.6.1", "version": "1.6.2",
"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": "github:PurpShell/Baileys#combined", "@whiskeysockets/baileys": "^6.5.0",
"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",

View File

@ -136,11 +136,22 @@ export type GlobalWebhook = {
ENABLED: boolean; ENABLED: boolean;
WEBHOOK_BY_EVENTS: boolean; WEBHOOK_BY_EVENTS: boolean;
}; };
export type CacheConfRedis = {
ENABLED: boolean;
URI: string;
PREFIX_KEY: string;
TTL: number;
};
export type CacheConfLocal = {
ENABLED: boolean;
TTL: number;
};
export type SslConf = { PRIVKEY: string; FULLCHAIN: string }; 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; KEEP_OPEN: boolean }; export type Typebot = { API_VERSION: string; KEEP_OPEN: boolean };
export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal };
export type Production = boolean; export type Production = boolean;
export interface Env { export interface Env {
@ -160,6 +171,7 @@ export interface Env {
CONFIG_SESSION_PHONE: ConfigSessionPhone; CONFIG_SESSION_PHONE: ConfigSessionPhone;
QRCODE: QrCode; QRCODE: QrCode;
TYPEBOT: Typebot; TYPEBOT: Typebot;
CACHE: CacheConf;
AUTHENTICATION: Auth; AUTHENTICATION: Auth;
PRODUCTION?: Production; PRODUCTION?: Production;
WABUSSINESS: WABussiness; WABUSSINESS: WABussiness;
@ -326,6 +338,18 @@ export class ConfigService {
API_VERSION: process.env?.TYPEBOT_API_VERSION || 'old', API_VERSION: process.env?.TYPEBOT_API_VERSION || 'old',
KEEP_OPEN: process.env.TYPEBOT_KEEP_OPEN === 'true', KEEP_OPEN: process.env.TYPEBOT_KEEP_OPEN === 'true',
}, },
CACHE: {
REDIS: {
ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true',
URI: process.env?.CACHE_REDIS_URI || '',
PREFIX_KEY: process.env?.CACHE_REDIS_PREFIX_KEY || 'evolution-cache',
TTL: Number.parseInt(process.env?.CACHE_REDIS_TTL) || 604800,
},
LOCAL: {
ENABLED: process.env?.CACHE_LOCAL_ENABLED === 'true',
TTL: Number.parseInt(process.env?.CACHE_REDIS_TTL) || 86400,
},
},
AUTHENTICATION: { AUTHENTICATION: {
TYPE: process.env.AUTHENTICATION_TYPE as 'apikey', TYPE: process.env.AUTHENTICATION_TYPE as 'apikey',
API_KEY: { API_KEY: {

View File

@ -162,6 +162,17 @@ TYPEBOT:
API_VERSION: 'old' # old | latest API_VERSION: 'old' # old | latest
KEEP_OPEN: false KEEP_OPEN: false
# Cache to optimize application performance
CACHE:
REDIS:
ENABLED: false
URI: "redis://localhost:6379"
PREFIX_KEY: "evolution-cache"
TTL: 604800
LOCAL:
ENABLED: false
TTL: 86400
# 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,
# if you use jwt, a random token will be generated and may be expired and you will have to generate a new token # if you use jwt, a random token will be generated and may be expired and you will have to generate a new 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.6.1 version: 1.6.2
contact: contact:
name: DavidsonGomes name: DavidsonGomes
email: contato@agenciadgcode.com email: contato@agenciadgcode.com

View File

@ -27,6 +27,7 @@ export const initAMQP = () => {
channel.assertExchange(exchangeName, 'topic', { channel.assertExchange(exchangeName, 'topic', {
durable: true, durable: true,
autoDelete: false, autoDelete: false,
assert: true,
}); });
amqpChannel = channel; amqpChannel = channel;
@ -43,7 +44,7 @@ export const getAMQP = (): amqp.Channel | null => {
}; };
export const initQueues = (instanceName: string, events: string[]) => { export const initQueues = (instanceName: string, events: string[]) => {
if (!events || !events.length) return; if (!instanceName || !events || !events.length) return;
const queues = events.map((event) => { const queues = events.map((event) => {
return `${event.replace(/_/g, '.').toLowerCase()}`; return `${event.replace(/_/g, '.').toLowerCase()}`;
@ -56,6 +57,7 @@ export const initQueues = (instanceName: string, events: string[]) => {
amqp.assertExchange(exchangeName, 'topic', { amqp.assertExchange(exchangeName, 'topic', {
durable: true, durable: true,
autoDelete: false, autoDelete: false,
assert: true,
}); });
const queueName = `${instanceName}.${event}`; const queueName = `${instanceName}.${event}`;
@ -89,6 +91,7 @@ export const removeQueues = (instanceName: string, events: string[]) => {
amqp.assertExchange(exchangeName, 'topic', { amqp.assertExchange(exchangeName, 'topic', {
durable: true, durable: true,
autoDelete: false, autoDelete: false,
assert: true,
}); });
const queueName = `${instanceName}.${event}`; const queueName = `${instanceName}.${event}`;

22
src/libs/cacheengine.ts Normal file
View File

@ -0,0 +1,22 @@
import { CacheConf, ConfigService } from '../config/env.config';
import { ICache } from '../whatsapp/abstract/abstract.cache';
import { LocalCache } from './localcache';
import { RedisCache } from './rediscache';
export class CacheEngine {
private engine: ICache;
constructor(private readonly configService: ConfigService, module: string) {
const cacheConf = configService.get<CacheConf>('CACHE');
if (cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') {
this.engine = new RedisCache(configService, module);
} else if (cacheConf?.LOCAL?.ENABLED) {
this.engine = new LocalCache(configService, module);
}
}
public getEngine() {
return this.engine;
}
}

48
src/libs/localcache.ts Normal file
View File

@ -0,0 +1,48 @@
import NodeCache from 'node-cache';
import { CacheConf, CacheConfLocal, ConfigService } from '../config/env.config';
import { ICache } from '../whatsapp/abstract/abstract.cache';
export class LocalCache implements ICache {
private conf: CacheConfLocal;
static localCache = new NodeCache();
constructor(private readonly configService: ConfigService, private readonly module: string) {
this.conf = this.configService.get<CacheConf>('CACHE')?.LOCAL;
}
async get(key: string): Promise<any> {
return LocalCache.localCache.get(this.buildKey(key));
}
async set(key: string, value: any, ttl?: number) {
return LocalCache.localCache.set(this.buildKey(key), value, ttl || this.conf.TTL);
}
async has(key: string) {
return LocalCache.localCache.has(this.buildKey(key));
}
async delete(key: string) {
return LocalCache.localCache.del(this.buildKey(key));
}
async deleteAll(appendCriteria?: string) {
const keys = await this.keys(appendCriteria);
if (!keys?.length) {
return 0;
}
return LocalCache.localCache.del(keys);
}
async keys(appendCriteria?: string) {
const filter = `${this.buildKey('')}${appendCriteria ? `${appendCriteria}:` : ''}`;
return LocalCache.localCache.keys().filter((key) => key.substring(0, filter.length) === filter);
}
buildKey(key: string) {
return `${this.module}:${key}`;
}
}

View File

@ -0,0 +1,59 @@
import { createClient, RedisClientType } from 'redis';
import { CacheConf, CacheConfRedis, configService } from '../config/env.config';
import { Logger } from '../config/logger.config';
class Redis {
private logger = new Logger(Redis.name);
private client: RedisClientType = null;
private conf: CacheConfRedis;
private connected = false;
constructor() {
this.conf = configService.get<CacheConf>('CACHE')?.REDIS;
}
getConnection(): RedisClientType {
if (this.connected) {
return this.client;
} else {
this.client = createClient({
url: this.conf.URI,
});
this.client.on('connect', () => {
this.logger.verbose('redis connecting');
});
this.client.on('ready', () => {
this.logger.verbose('redis ready');
this.connected = true;
});
this.client.on('error', () => {
this.logger.error('redis disconnected');
this.connected = false;
});
this.client.on('end', () => {
this.logger.verbose('redis connection ended');
this.connected = false;
});
try {
this.logger.verbose('connecting new redis client');
this.client.connect();
this.connected = true;
this.logger.verbose('connected to new redis client');
} catch (e) {
this.connected = false;
this.logger.error('redis connect exception caught: ' + e);
return null;
}
return this.client;
}
}
}
export const redisClient = new Redis();

83
src/libs/rediscache.ts Normal file
View File

@ -0,0 +1,83 @@
import { RedisClientType } from 'redis';
import { CacheConf, CacheConfRedis, ConfigService } from '../config/env.config';
import { Logger } from '../config/logger.config';
import { ICache } from '../whatsapp/abstract/abstract.cache';
import { redisClient } from './rediscache.client';
export class RedisCache implements ICache {
private readonly logger = new Logger(RedisCache.name);
private client: RedisClientType;
private conf: CacheConfRedis;
constructor(private readonly configService: ConfigService, private readonly module: string) {
this.conf = this.configService.get<CacheConf>('CACHE')?.REDIS;
this.client = redisClient.getConnection();
}
async get(key: string): Promise<any> {
try {
return JSON.parse(await this.client.get(this.buildKey(key)));
} catch (error) {
this.logger.error(error);
}
}
async set(key: string, value: any, ttl?: number) {
try {
await this.client.setEx(this.buildKey(key), ttl || this.conf?.TTL, JSON.stringify(value));
} catch (error) {
this.logger.error(error);
}
}
async has(key: string) {
try {
return (await this.client.exists(this.buildKey(key))) > 0;
} catch (error) {
this.logger.error(error);
}
}
async delete(key: string) {
try {
return await this.client.del(this.buildKey(key));
} catch (error) {
this.logger.error(error);
}
}
async deleteAll(appendCriteria?: string) {
try {
const keys = await this.keys(appendCriteria);
if (!keys?.length) {
return 0;
}
return await this.client.del(keys);
} catch (error) {
this.logger.error(error);
}
}
async keys(appendCriteria?: string) {
try {
const match = `${this.buildKey('')}${appendCriteria ? `${appendCriteria}:` : ''}*`;
const keys = [];
for await (const key of this.client.scanIterator({
MATCH: match,
COUNT: 100,
})) {
keys.push(key);
}
return [...new Set(keys)];
} catch (error) {
this.logger.error(error);
}
}
buildKey(key: string) {
return `${this.conf?.PREFIX_KEY}:${this.module}:${key}`;
}
}

View File

@ -611,6 +611,26 @@ export const profileStatusSchema: JSONSchema7 = {
...isNotEmpty('status'), ...isNotEmpty('status'),
}; };
export const updateMessageSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
number: { type: 'string' },
text: { type: 'string' },
key: {
type: 'object',
properties: {
id: { type: 'string' },
remoteJid: { type: 'string' },
fromMe: { type: 'boolean', enum: [true, false] },
},
required: ['id', 'fromMe', 'remoteJid'],
...isNotEmpty('id', 'remoteJid'),
},
},
...isNotEmpty('number', 'text', 'key'),
};
export const profilePictureSchema: JSONSchema7 = { export const profilePictureSchema: JSONSchema7 = {
$id: v4(), $id: v4(),
type: 'object', type: 'object',
@ -1127,7 +1147,18 @@ export const proxySchema: JSONSchema7 = {
type: 'object', type: 'object',
properties: { properties: {
enabled: { type: 'boolean', enum: [true, false] }, enabled: { type: 'boolean', enum: [true, false] },
proxy: { type: 'string' }, proxy: {
type: 'object',
properties: {
host: { type: 'string' },
port: { type: 'string' },
protocol: { type: 'string' },
username: { type: 'string' },
password: { type: 'string' },
},
required: ['host', 'port', 'protocol'],
...isNotEmpty('host', 'port', 'protocol'),
},
}, },
required: ['enabled', 'proxy'], required: ['enabled', 'proxy'],
...isNotEmpty('enabled', 'proxy'), ...isNotEmpty('enabled', 'proxy'),

View File

@ -0,0 +1,13 @@
export interface ICache {
get(key: string): Promise<any>;
set(key: string, value: any, ttl?: number): void;
has(key: string): Promise<boolean>;
keys(appendCriteria?: string): Promise<string[]>;
delete(key: string | string[]): Promise<number>;
deleteAll(appendCriteria?: string): Promise<number>;
}

View File

@ -10,6 +10,7 @@ import {
ProfileStatusDto, ProfileStatusDto,
ReadMessageDto, ReadMessageDto,
SendPresenceDto, SendPresenceDto,
UpdateMessageDto,
WhatsAppNumberDto, WhatsAppNumberDto,
NumberBusiness, NumberBusiness,
} from '../dto/chat.dto'; } from '../dto/chat.dto';
@ -123,4 +124,9 @@ export class ChatController {
logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance'); logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance');
return await this.waMonitor.waInstances[instanceName].setWhatsappBusinessProfile(data); return await this.waMonitor.waInstances[instanceName].setWhatsappBusinessProfile(data);
} }
public async updateMessage({ instanceName }: InstanceDto, data: UpdateMessageDto) {
logger.verbose('requested updateMessage from ' + instanceName + ' instance');
return await this.waMonitor.waInstances[instanceName].updateMessage(data);
}
} }

View File

@ -3,9 +3,11 @@ import { isURL } from 'class-validator';
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';
import { BadRequestException } from '../../exceptions'; import { BadRequestException } from '../../exceptions';
import { CacheEngine } from '../../libs/cacheengine';
import { ChatwootDto } from '../dto/chatwoot.dto'; import { ChatwootDto } from '../dto/chatwoot.dto';
import { InstanceDto } from '../dto/instance.dto'; import { InstanceDto } from '../dto/instance.dto';
import { RepositoryBroker } from '../repository/repository.manager'; import { RepositoryBroker } from '../repository/repository.manager';
import { CacheService } from '../services/cache.service';
import { ChatwootService } from '../services/chatwoot.service'; import { ChatwootService } from '../services/chatwoot.service';
import { waMonitor } from '../whatsapp.module'; import { waMonitor } from '../whatsapp.module';
@ -94,7 +96,9 @@ export class ChatwootController {
public async receiveWebhook(instance: InstanceDto, data: any) { public async receiveWebhook(instance: InstanceDto, data: any) {
logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance'); logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance');
const chatwootService = new ChatwootService(waMonitor, this.configService, this.repository);
const chatwootCache = new CacheService(new CacheEngine(this.configService, ChatwootService.name).getEngine());
const chatwootService = new ChatwootService(waMonitor, this.configService, this.repository, chatwootCache);
return chatwootService.receiveWebhook(instance, data); return chatwootService.receiveWebhook(instance, data);
} }

View File

@ -10,9 +10,9 @@ import { RedisCache } from '../../libs/redis.client';
import { InstanceDto } from '../dto/instance.dto'; import { InstanceDto } from '../dto/instance.dto';
import { RepositoryBroker } from '../repository/repository.manager'; import { RepositoryBroker } from '../repository/repository.manager';
import { AuthService, OldToken } from '../services/auth.service'; import { AuthService, OldToken } from '../services/auth.service';
import { CacheService } from '../services/cache.service';
import { ChatwootService } from '../services/chatwoot.service'; import { ChatwootService } from '../services/chatwoot.service';
import { WAMonitoringService } from '../services/monitor.service'; import { WAMonitoringService } from '../services/monitor.service';
import { ProxyService } from '../services/proxy.service';
import { RabbitmqService } from '../services/rabbitmq.service'; import { RabbitmqService } from '../services/rabbitmq.service';
import { SettingsService } from '../services/settings.service'; import { SettingsService } from '../services/settings.service';
import { SqsService } from '../services/sqs.service'; import { SqsService } from '../services/sqs.service';
@ -34,10 +34,10 @@ export class InstanceController {
private readonly settingsService: SettingsService, private readonly settingsService: SettingsService,
private readonly websocketService: WebsocketService, private readonly websocketService: WebsocketService,
private readonly rabbitmqService: RabbitmqService, private readonly rabbitmqService: RabbitmqService,
private readonly proxyService: ProxyService,
private readonly sqsService: SqsService, private readonly sqsService: SqsService,
private readonly typebotService: TypebotService, private readonly typebotService: TypebotService,
private readonly cache: RedisCache, private readonly cache: RedisCache,
private readonly chatwootCache: CacheService,
) {} ) {}
private readonly logger = new Logger(InstanceController.name); private readonly logger = new Logger(InstanceController.name);
@ -77,7 +77,6 @@ export class InstanceController {
typebot_delay_message, typebot_delay_message,
typebot_unknown_message, typebot_unknown_message,
typebot_listening_from_me, typebot_listening_from_me,
proxy,
}: InstanceDto) { }: InstanceDto) {
try { try {
this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); this.logger.verbose('requested createInstance from ' + instanceName + ' instance');
@ -86,7 +85,15 @@ export class InstanceController {
await this.authService.checkDuplicateToken(token); await this.authService.checkDuplicateToken(token);
this.logger.verbose('creating instance'); this.logger.verbose('creating instance');
const instance = new WAStartupClass[integration](this.configService, this.eventEmitter, this.repository, this.cache);
const instance = new WAStartupService(
this.configService,
this.eventEmitter,
this.repository,
this.cache,
this.chatwootCache,
);
instance.instanceName = instanceName; instance.instanceName = instanceName;
instance.instanceNumber = number; instance.instanceNumber = number;
instance.instanceToken = token; instance.instanceToken = token;
@ -261,22 +268,6 @@ export class InstanceController {
} }
} }
if (proxy) {
this.logger.verbose('creating proxy');
try {
this.proxyService.create(
instance,
{
enabled: true,
proxy,
},
false,
);
} catch (error) {
this.logger.log(error);
}
}
let sqsEvents: string[]; let sqsEvents: string[];
if (sqs_enabled) { if (sqs_enabled) {
@ -419,7 +410,6 @@ export class InstanceController {
settings, settings,
webhook_url: webhook_url, webhook_url: webhook_url,
qrcode: getQrcode, qrcode: getQrcode,
proxy,
}; };
this.logger.verbose('instance created'); this.logger.verbose('instance created');
@ -525,7 +515,6 @@ export class InstanceController {
name_inbox: instance.instanceName, name_inbox: instance.instanceName,
webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
}, },
proxy,
}; };
} catch (error) { } catch (error) {
this.logger.error(error.message[0]); this.logger.error(error.message[0]);
@ -584,6 +573,7 @@ export class InstanceController {
switch (state) { switch (state) {
case 'open': case 'open':
this.logger.verbose('logging out instance: ' + instanceName); this.logger.verbose('logging out instance: ' + instanceName);
instance.clearCacheChatwoot();
await instance.reloadConnection(); await instance.reloadConnection();
await delay(2000); await delay(2000);
@ -649,6 +639,7 @@ export class InstanceController {
} }
try { try {
this.waMonitor.waInstances[instanceName]?.removeRabbitmqQueues(); this.waMonitor.waInstances[instanceName]?.removeRabbitmqQueues();
this.waMonitor.waInstances[instanceName]?.clearCacheChatwoot();
if (instance.state === 'connecting') { if (instance.state === 'connecting') {
this.logger.verbose('logging out instance: ' + instanceName); this.logger.verbose('logging out instance: ' + instanceName);
@ -658,10 +649,15 @@ export class InstanceController {
this.logger.verbose('deleting instance: ' + instanceName); this.logger.verbose('deleting instance: ' + instanceName);
this.waMonitor.waInstances[instanceName].sendDataWebhook(Events.INSTANCE_DELETE, { try {
instanceName, this.waMonitor.waInstances[instanceName].sendDataWebhook(Events.INSTANCE_DELETE, {
instanceId: (await this.repository.auth.find(instanceName))?.instanceId, instanceName,
}); instanceId: (await this.repository.auth.find(instanceName))?.instanceId,
});
} catch (error) {
this.logger.error(error);
}
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

@ -1,4 +1,7 @@
import axios from 'axios';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import { BadRequestException } from '../../exceptions';
import { InstanceDto } from '../dto/instance.dto'; import { InstanceDto } from '../dto/instance.dto';
import { ProxyDto } from '../dto/proxy.dto'; import { ProxyDto } from '../dto/proxy.dto';
import { ProxyService } from '../services/proxy.service'; import { ProxyService } from '../services/proxy.service';
@ -13,7 +16,16 @@ export class ProxyController {
if (!data.enabled) { if (!data.enabled) {
logger.verbose('proxy disabled'); logger.verbose('proxy disabled');
data.proxy = ''; data.proxy = null;
}
if (data.proxy) {
logger.verbose('proxy enabled');
const { host, port, protocol, username, password } = data.proxy;
const testProxy = await this.testProxy(host, port, protocol, username, password);
if (!testProxy) {
throw new BadRequestException('Invalid proxy');
}
} }
return this.proxyService.create(instance, data); return this.proxyService.create(instance, data);
@ -23,4 +35,36 @@ export class ProxyController {
logger.verbose('requested findProxy from ' + instance.instanceName + ' instance'); logger.verbose('requested findProxy from ' + instance.instanceName + ' instance');
return this.proxyService.find(instance); return this.proxyService.find(instance);
} }
private async testProxy(host: string, port: string, protocol: string, username?: string, password?: string) {
logger.verbose('requested testProxy');
try {
let proxyConfig: any = {
host: host,
port: parseInt(port),
protocol: protocol,
};
if (username && password) {
proxyConfig = {
...proxyConfig,
auth: {
username: username,
password: password,
},
};
}
const serverIp = await axios.get('http://meuip.com/api/meuip.php');
const response = await axios.get('http://meuip.com/api/meuip.php', {
proxy: proxyConfig,
});
logger.verbose('testProxy response: ' + response.data);
return response.data !== serverIp.data;
} catch (error) {
logger.error('testProxy error: ' + error);
return false;
}
}
} }

View File

@ -103,3 +103,9 @@ export class SendPresenceDto extends Metadata {
delay: number; delay: number;
}; };
} }
export class UpdateMessageDto extends Metadata {
number: string;
key: proto.IMessageKey;
text: string;
}

View File

@ -1,4 +1,12 @@
class Proxy {
host: string;
port: string;
protocol: string;
username?: string;
password?: string;
}
export class ProxyDto { export class ProxyDto {
enabled: boolean; enabled: boolean;
proxy: string; proxy: Proxy;
} }

View File

@ -14,6 +14,7 @@ class ChatwootMessage {
messageId?: number; messageId?: number;
inboxId?: number; inboxId?: number;
conversationId?: number; conversationId?: number;
contactInbox?: { sourceId: string };
} }
export class MessageRaw { export class MessageRaw {
@ -29,6 +30,7 @@ export class MessageRaw {
source_id?: string; source_id?: string;
source_reply_id?: string; source_reply_id?: string;
chatwoot?: ChatwootMessage; chatwoot?: ChatwootMessage;
contextInfo?: any;
} }
const messageSchema = new Schema<MessageRaw>({ const messageSchema = new Schema<MessageRaw>({
@ -50,6 +52,7 @@ const messageSchema = new Schema<MessageRaw>({
messageId: { type: Number }, messageId: { type: Number },
inboxId: { type: Number }, inboxId: { type: Number },
conversationId: { type: Number }, conversationId: { type: Number },
contactInbox: { type: Object },
}, },
}); });

View File

@ -2,16 +2,30 @@ import { Schema } from 'mongoose';
import { dbserver } from '../../libs/db.connect'; import { dbserver } from '../../libs/db.connect';
class Proxy {
host?: string;
port?: string;
protocol?: string;
username?: string;
password?: string;
}
export class ProxyRaw { export class ProxyRaw {
_id?: string; _id?: string;
enabled?: boolean; enabled?: boolean;
proxy?: string; proxy?: Proxy;
} }
const proxySchema = new Schema<ProxyRaw>({ const proxySchema = new Schema<ProxyRaw>({
_id: { type: String, _id: true }, _id: { type: String, _id: true },
enabled: { type: Boolean, required: true }, enabled: { type: Boolean, required: true },
proxy: { type: String, required: true }, proxy: {
host: { type: String, required: true },
port: { type: String, required: true },
protocol: { type: String, required: true },
username: { type: String, required: false },
password: { type: String, required: false },
},
}); });
export const ProxyModel = dbserver?.model(ProxyRaw.name, proxySchema, 'proxy'); export const ProxyModel = dbserver?.model(ProxyRaw.name, proxySchema, 'proxy');

View File

@ -1,4 +1,4 @@
import { opendirSync, readFileSync } from 'fs'; import { opendirSync, readFileSync, rmSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { ConfigService, StoreConf } from '../../config/env.config'; import { ConfigService, StoreConf } from '../../config/env.config';
@ -18,6 +18,19 @@ export class MessageRepository extends Repository {
private readonly logger = new Logger('MessageRepository'); private readonly logger = new Logger('MessageRepository');
public buildQuery(query: MessageQuery): MessageQuery {
for (const [o, p] of Object.entries(query?.where)) {
if (typeof p === 'object' && p !== null && !Array.isArray(p)) {
for (const [k, v] of Object.entries(p)) {
query.where[`${o}.${k}`] = v;
}
delete query.where[o];
}
}
return query;
}
public async insert(data: MessageRaw[], instanceName: string, saveDb = false): Promise<IInsert> { public async insert(data: MessageRaw[], instanceName: string, saveDb = false): Promise<IInsert> {
this.logger.verbose('inserting messages'); this.logger.verbose('inserting messages');
@ -91,14 +104,7 @@ 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');
for (const [o, p] of Object.entries(query?.where)) { query = this.buildQuery(query);
if (typeof p === 'object' && p !== null && !Array.isArray(p)) {
for (const [k, v] of Object.entries(p)) {
query.where[`${o}.${k}`] = v;
}
delete query.where[o];
}
}
return await this.messageModel return await this.messageModel
.find({ ...query.where }) .find({ ...query.where })
@ -198,15 +204,23 @@ export class MessageRepository extends Repository {
} }
} }
public async delete(query: any) { public async delete(query: MessageQuery) {
try { try {
this.logger.verbose('deleting messages'); this.logger.verbose('deleting message');
if (this.dbSettings.ENABLED) { if (this.dbSettings.ENABLED) {
this.logger.verbose('deleting messages in db'); this.logger.verbose('deleting message in db');
return await this.messageModel.deleteMany(query); query = this.buildQuery(query);
return await this.messageModel.deleteOne({ ...query.where });
} }
return { deleted: { chatId: query.where.messageTimestamp } }; this.logger.verbose('deleting message in store');
rmSync(join(this.storePath, 'messages', query.where.owner, query.where.key.id + '.json'), {
force: true,
recursive: true,
});
return { deleted: { messageId: query.where.key.id } };
} catch (error) { } catch (error) {
return { error: error?.toString() }; return { error: error?.toString() };
} }

View File

@ -14,6 +14,7 @@ import {
profileSchema, profileSchema,
profileStatusSchema, profileStatusSchema,
readMessageSchema, readMessageSchema,
updateMessageSchema,
whatsappNumberSchema, whatsappNumberSchema,
profileBusinessSchema, profileBusinessSchema,
} from '../../validate/validate.schema'; } from '../../validate/validate.schema';
@ -29,6 +30,7 @@ import {
ProfileStatusDto, ProfileStatusDto,
ReadMessageDto, ReadMessageDto,
SendPresenceDto, SendPresenceDto,
UpdateMessageDto,
WhatsAppNumberDto, WhatsAppNumberDto,
NumberBusiness, NumberBusiness,
} from '../dto/chat.dto'; } from '../dto/chat.dto';
@ -383,6 +385,23 @@ export class ChatRouter extends RouterBroker {
execute: (instance) => chatController.removeProfilePicture(instance), execute: (instance) => chatController.removeProfilePicture(instance),
}); });
return res.status(HttpStatus.OK).json(response);
})
.put(this.routerPath('updateMessage'), ...guards, async (req, res) => {
logger.verbose('request received in updateMessage');
logger.verbose('request body: ');
logger.verbose(req.body);
logger.verbose('request query: ');
logger.verbose(req.query);
const response = await this.dataValidate<UpdateMessageDto>({
request: req,
schema: updateMessageSchema,
ClassRef: UpdateMessageDto,
execute: (instance, data) => chatController.updateMessage(instance, data),
});
return res.status(HttpStatus.OK).json(response); return res.status(HttpStatus.OK).json(response);
}); });
} }

View File

@ -0,0 +1,62 @@
import { Logger } from '../../config/logger.config';
import { ICache } from '../abstract/abstract.cache';
export class CacheService {
private readonly logger = new Logger(CacheService.name);
constructor(private readonly cache: ICache) {
if (cache) {
this.logger.verbose(`cacheservice created using cache engine: ${cache.constructor?.name}`);
} else {
this.logger.verbose(`cacheservice disabled`);
}
}
async get(key: string): Promise<any> {
if (!this.cache) {
return;
}
this.logger.verbose(`cacheservice getting key: ${key}`);
return this.cache.get(key);
}
async set(key: string, value: any) {
if (!this.cache) {
return;
}
this.logger.verbose(`cacheservice setting key: ${key}`);
this.cache.set(key, value);
}
async has(key: string) {
if (!this.cache) {
return;
}
this.logger.verbose(`cacheservice has key: ${key}`);
return this.cache.has(key);
}
async delete(key: string) {
if (!this.cache) {
return;
}
this.logger.verbose(`cacheservice deleting key: ${key}`);
return this.cache.delete(key);
}
async deleteAll(appendCriteria?: string) {
if (!this.cache) {
return;
}
this.logger.verbose(`cacheservice deleting all keys`);
return this.cache.deleteAll(appendCriteria);
}
async keys(appendCriteria?: string) {
if (!this.cache) {
return;
}
this.logger.verbose(`cacheservice getting all keys`);
return this.cache.keys(appendCriteria);
}
}

View File

@ -1,26 +1,24 @@
import ChatwootClient from '@figuro/chatwoot-sdk'; import ChatwootClient, { ChatwootAPIConfig, contact, conversation, inbox } from '@figuro/chatwoot-sdk';
import { request as chatwootRequest } from '@figuro/chatwoot-sdk/dist/core/request';
import axios from 'axios'; import axios from 'axios';
import FormData from 'form-data'; import FormData from 'form-data';
import { createReadStream, readFileSync, unlinkSync, writeFileSync } from 'fs'; import { createReadStream, unlinkSync, writeFileSync } from 'fs';
import Jimp from 'jimp'; import Jimp from 'jimp';
import mimeTypes from 'mime-types'; import mimeTypes from 'mime-types';
import path from 'path'; import path from 'path';
import { ConfigService, HttpServer, WABussiness } from '../../config/env.config'; import { ConfigService, HttpServer, WABussiness } from '../../config/env.config';
import { Logger } from '../../config/logger.config'; import { Logger } from '../../config/logger.config';
import { ROOT_DIR } from '../../config/path.config'; import { ICache } from '../abstract/abstract.cache';
import { ChatwootDto } from '../dto/chatwoot.dto'; import { ChatwootDto } from '../dto/chatwoot.dto';
import { InstanceDto } from '../dto/instance.dto'; import { InstanceDto } from '../dto/instance.dto';
import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto, SendTemplateDto } from '../dto/sendMessage.dto'; import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto, SendTemplateDto } from '../dto/sendMessage.dto';
import { MessageRaw } from '../models'; import { ChatwootRaw, MessageRaw } from '../models';
import { RepositoryBroker } from '../repository/repository.manager'; import { RepositoryBroker } from '../repository/repository.manager';
import { Events } from '../types/wa.types'; import { Events } from '../types/wa.types';
import { WAMonitoringService } from './monitor.service'; import { WAMonitoringService } from './monitor.service';
export class ChatwootService { export class ChatwootService {
private messageCacheFile: string;
private messageCache: Set<string>;
private readonly logger = new Logger(ChatwootService.name); private readonly logger = new Logger(ChatwootService.name);
private provider: any; private provider: any;
@ -29,35 +27,15 @@ export class ChatwootService {
private readonly waMonitor: WAMonitoringService, private readonly waMonitor: WAMonitoringService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly repository: RepositoryBroker, private readonly repository: RepositoryBroker,
) { private readonly cache: ICache,
this.messageCache = new Set(); ) {}
}
private loadMessageCache(): Set<string> {
this.logger.verbose('load message cache');
try {
const cacheData = readFileSync(this.messageCacheFile, 'utf-8');
const cacheArray = cacheData.split('\n');
return new Set(cacheArray);
} catch (error) {
return new Set();
}
}
private saveMessageCache() {
this.logger.verbose('save message cache');
const cacheData = Array.from(this.messageCache).join('\n');
writeFileSync(this.messageCacheFile, cacheData, 'utf-8');
this.logger.verbose('message cache saved');
}
private clearMessageCache() {
this.logger.verbose('clear message cache');
this.messageCache.clear();
this.saveMessageCache();
}
private async getProvider(instance: InstanceDto) { private async getProvider(instance: InstanceDto) {
const cacheKey = `${instance.instanceName}:getProvider`;
if (await this.cache.has(cacheKey)) {
return (await this.cache.get(cacheKey)) as ChatwootRaw;
}
this.logger.verbose('get provider to instance: ' + instance.instanceName); this.logger.verbose('get provider to instance: ' + instance.instanceName);
const provider = await this.waMonitor.waInstances[instance.instanceName]?.findChatwoot(); const provider = await this.waMonitor.waInstances[instance.instanceName]?.findChatwoot();
@ -68,6 +46,8 @@ export class ChatwootService {
this.logger.verbose('provider found'); this.logger.verbose('provider found');
this.cache.set(cacheKey, provider);
return provider; return provider;
// try { // try {
// } catch (error) { // } catch (error) {
@ -92,12 +72,7 @@ export class ChatwootService {
this.logger.verbose('create client to instance: ' + instance.instanceName); this.logger.verbose('create client to instance: ' + instance.instanceName);
const client = new ChatwootClient({ const client = new ChatwootClient({
config: { config: this.getClientCwConfig(),
basePath: provider.url,
with_credentials: true,
credentials: 'include',
token: provider.token,
},
}); });
this.logger.verbose('client created'); this.logger.verbose('client created');
@ -105,6 +80,19 @@ export class ChatwootService {
return client; return client;
} }
public getClientCwConfig(): ChatwootAPIConfig {
return {
basePath: this.provider.url,
with_credentials: true,
credentials: 'include',
token: this.provider.token,
};
}
public getCache() {
return this.cache;
}
public async create(instance: InstanceDto, data: ChatwootDto) { public async create(instance: InstanceDto, data: ChatwootDto) {
this.logger.verbose('create chatwoot: ' + instance.instanceName); this.logger.verbose('create chatwoot: ' + instance.instanceName);
@ -419,6 +407,26 @@ export class ChatwootService {
return null; return null;
} }
const cacheKey = `${instance.instanceName}:createConversation-${body.key.remoteJid}`;
if (await this.cache.has(cacheKey)) {
const conversationId = (await this.cache.get(cacheKey)) as number;
let conversationExists: conversation | boolean;
try {
conversationExists = await client.conversations.get({
accountId: this.provider.account_id,
conversationId: conversationId,
});
} catch (error) {
conversationExists = false;
}
if (!conversationExists) {
this.cache.delete(cacheKey);
return await this.createConversation(instance, body);
}
return conversationId;
}
const isGroup = body.key.remoteJid.includes('@g.us'); const isGroup = body.key.remoteJid.includes('@g.us');
this.logger.verbose('is group: ' + isGroup); this.logger.verbose('is group: ' + isGroup);
@ -569,6 +577,7 @@ export class ChatwootService {
if (conversation) { if (conversation) {
this.logger.verbose('conversation found'); this.logger.verbose('conversation found');
this.cache.set(cacheKey, conversation.id);
return conversation.id; return conversation.id;
} }
} }
@ -594,6 +603,7 @@ export class ChatwootService {
} }
this.logger.verbose('conversation created'); this.logger.verbose('conversation created');
this.cache.set(cacheKey, conversation.id);
return conversation.id; return conversation.id;
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
@ -603,6 +613,11 @@ export class ChatwootService {
public async getInbox(instance: InstanceDto) { public async getInbox(instance: InstanceDto) {
this.logger.verbose('get inbox to instance: ' + instance.instanceName); this.logger.verbose('get inbox to instance: ' + instance.instanceName);
const cacheKey = `${instance.instanceName}:getInbox`;
if (await this.cache.has(cacheKey)) {
return (await this.cache.get(cacheKey)) as inbox;
}
const client = await this.clientCw(instance); const client = await this.clientCw(instance);
if (!client) { if (!client) {
@ -629,6 +644,7 @@ export class ChatwootService {
} }
this.logger.verbose('return inbox'); this.logger.verbose('return inbox');
this.cache.set(cacheKey, findByName);
return findByName; return findByName;
} }
@ -644,6 +660,7 @@ export class ChatwootService {
filename: string; filename: string;
}[], }[],
messageBody?: any, messageBody?: any,
sourceId?: string,
) { ) {
this.logger.verbose('create message to instance: ' + instance.instanceName); this.logger.verbose('create message to instance: ' + instance.instanceName);
@ -665,6 +682,7 @@ export class ChatwootService {
message_type: messageType, message_type: messageType,
attachments: attachments, attachments: attachments,
private: privateMessage || false, private: privateMessage || false,
source_id: sourceId,
content_attributes: { content_attributes: {
...replyToIds, ...replyToIds,
}, },
@ -765,6 +783,7 @@ export class ChatwootService {
content?: string, content?: string,
instance?: InstanceDto, instance?: InstanceDto,
messageBody?: any, messageBody?: any,
sourceId?: string,
) { ) {
this.logger.verbose('send data to chatwoot'); this.logger.verbose('send data to chatwoot');
@ -791,6 +810,10 @@ export class ChatwootService {
} }
} }
if (sourceId) {
data.append('source_id', sourceId);
}
this.logger.verbose('get client to instance: ' + this.provider.instanceName); this.logger.verbose('get client to instance: ' + this.provider.instanceName);
const config = { const config = {
method: 'post', method: 'post',
@ -916,17 +939,21 @@ export class ChatwootService {
try { try {
this.logger.verbose('get media type'); this.logger.verbose('get media type');
const parts = media.split('/'); const parsedMedia = path.parse(decodeURIComponent(media));
let mimeType = mimeTypes.lookup(parsedMedia?.ext) || '';
let fileName = parsedMedia?.name + parsedMedia?.ext;
const fileName = decodeURIComponent(parts[parts.length - 1]); if (!mimeType) {
this.logger.verbose('file name: ' + fileName); const parts = media.split('/');
fileName = decodeURIComponent(parts[parts.length - 1]);
this.logger.verbose('file name: ' + fileName);
const response = await axios.get(media, { const response = await axios.get(media, {
responseType: 'arraybuffer', responseType: 'arraybuffer',
}); });
mimeType = response.headers['content-type'];
const mimeType = response.headers['content-type']; this.logger.verbose('mime type: ' + mimeType);
this.logger.verbose('mime type: ' + mimeType); }
let type = 'document'; let type = 'document';
@ -1008,6 +1035,17 @@ export class ChatwootService {
return null; return null;
} }
// invalidate the conversation cache if reopen_conversation is false and the conversation was resolved
if (
this.provider.reopen_conversation === false &&
body.event === 'conversation_status_changed' &&
body.status === 'resolved' &&
body.meta?.sender?.identifier
) {
const keyToDelete = `${instance.instanceName}:createConversation-${body.meta.sender.identifier}`;
this.cache.delete(keyToDelete);
}
this.logger.verbose('check if is bot'); this.logger.verbose('check if is bot');
if ( if (
!body?.conversation || !body?.conversation ||
@ -1029,7 +1067,7 @@ export class ChatwootService {
.replaceAll(/(?<!`)`((?!\s)([^`*]+?)(?<!\s))`(?!`)/g, '```$1```') // Substitui ` por ``` .replaceAll(/(?<!`)`((?!\s)([^`*]+?)(?<!\s))`(?!`)/g, '```$1```') // Substitui ` por ```
: body.content; : body.content;
const senderName = body?.sender?.available_name || body?.sender?.name; const senderName = body?.conversation?.messages[0]?.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');
@ -1044,7 +1082,18 @@ export class ChatwootService {
limit: 1, limit: 1,
}); });
if (message.length && message[0].key?.id) { if (message.length && message[0].key?.id) {
this.logger.verbose('deleting message in whatsapp. Message id: ' + message[0].key.id);
await waInstance?.client?.sendMessage(message[0].key.remoteJid, { delete: message[0].key }); await waInstance?.client?.sendMessage(message[0].key.remoteJid, { delete: message[0].key });
this.logger.verbose('deleting message in repository. Message id: ' + message[0].key.id);
this.repository.message.delete({
where: {
owner: instance.instanceName,
chatwoot: {
messageId: body.id,
},
},
});
} }
return { message: 'bot' }; return { message: 'bot' };
} }
@ -1105,22 +1154,11 @@ export class ChatwootService {
if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') { if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') {
this.logger.verbose('check if is group'); this.logger.verbose('check if is group');
this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); if (body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:') {
this.logger.verbose('cache file path: ' + this.messageCacheFile); this.logger.verbose('message sent directly from whatsapp. Webhook ignored.');
this.messageCache = this.loadMessageCache();
this.logger.verbose('cache file loaded');
this.logger.verbose(this.messageCache);
this.logger.verbose('check if message is cached');
if (this.messageCache.has(body.id.toString())) {
this.logger.verbose('message is cached');
return { message: 'bot' }; return { message: 'bot' };
} }
this.logger.verbose('clear cache');
this.clearMessageCache();
this.logger.verbose('Format message to send'); this.logger.verbose('Format message to send');
let formatText: string; let formatText: string;
const regex = /^▶️.*◀️$/; const regex = /^▶️.*◀️$/;
@ -1197,6 +1235,9 @@ export class ChatwootService {
messageId: body.id, messageId: body.id,
inboxId: body.inbox?.id, inboxId: body.inbox?.id,
conversationId: body.conversation?.id, conversationId: body.conversation?.id,
contactInbox: {
sourceId: body.conversation?.contact_inbox?.source_id,
},
}, },
instance, instance,
); );
@ -1228,6 +1269,9 @@ export class ChatwootService {
messageId: body.id, messageId: body.id,
inboxId: body.inbox?.id, inboxId: body.inbox?.id,
conversationId: body.conversation?.id, conversationId: body.conversation?.id,
contactInbox: {
sourceId: body.conversation?.contact_inbox?.source_id,
},
}, },
instance, instance,
); );
@ -1396,6 +1440,8 @@ export class ChatwootService {
contactsArrayMessage: msg.contactsArrayMessage, contactsArrayMessage: msg.contactsArrayMessage,
locationMessage: msg.locationMessage, locationMessage: msg.locationMessage,
liveLocationMessage: msg.liveLocationMessage, liveLocationMessage: msg.liveLocationMessage,
listMessage: msg.listMessage,
listResponseMessage: msg.listResponseMessage,
}; };
this.logger.verbose('type message: ' + types); this.logger.verbose('type message: ' + types);
@ -1413,11 +1459,27 @@ export class ChatwootService {
const latitude = result.degreesLatitude; const latitude = result.degreesLatitude;
const longitude = result.degreesLongitude; const longitude = result.degreesLongitude;
const formattedLocation = `**Location:** const locationName = result?.name || 'Unknown';
**latitude:** ${latitude} const locationAddress = result?.address || 'Unknown';
**longitude:** ${longitude}
https://www.google.com/maps/search/?api=1&query=${latitude},${longitude} const formattedLocation =
`; '*Localização:*\n\n' +
'_Latitude:_ ' +
latitude +
'\n' +
'_Longitude:_ ' +
longitude +
'\n' +
'_Nome:_ ' +
locationName +
'\n' +
'_Endereço:_ ' +
locationAddress +
'\n' +
'_Url:_ https://www.google.com/maps/search/?api=1&query=' +
latitude +
',' +
longitude;
this.logger.verbose('message content: ' + formattedLocation); this.logger.verbose('message content: ' + formattedLocation);
@ -1435,19 +1497,17 @@ export class ChatwootService {
} }
}); });
let formattedContact = `**Contact:** let formattedContact = '*Contact:*\n\n' + '_Name:_ ' + contactInfo['FN'];
**name:** ${contactInfo['FN']}`;
let numberCount = 1; let numberCount = 1;
Object.keys(contactInfo).forEach((key) => { Object.keys(contactInfo).forEach((key) => {
if (key.startsWith('item') && key.includes('TEL')) { if (key.startsWith('item') && key.includes('TEL')) {
const phoneNumber = contactInfo[key]; const phoneNumber = contactInfo[key];
formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; formattedContact += '\n_Number (' + numberCount + '):_ ' + phoneNumber;
numberCount++; numberCount++;
} } else if (key.includes('TEL')) {
if (key.includes('TEL')) {
const phoneNumber = contactInfo[key]; const phoneNumber = contactInfo[key];
formattedContact += `\n**number:** ${phoneNumber}`; formattedContact += '\n_Number (' + numberCount + '):_ ' + phoneNumber;
numberCount++; numberCount++;
} }
}); });
@ -1468,19 +1528,17 @@ export class ChatwootService {
} }
}); });
let formattedContact = `**Contact:** let formattedContact = '*Contact:*\n\n' + '_Name:_ ' + contact.displayName;
**name:** ${contact.displayName}`;
let numberCount = 1; let numberCount = 1;
Object.keys(contactInfo).forEach((key) => { Object.keys(contactInfo).forEach((key) => {
if (key.startsWith('item') && key.includes('TEL')) { if (key.startsWith('item') && key.includes('TEL')) {
const phoneNumber = contactInfo[key]; const phoneNumber = contactInfo[key];
formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; formattedContact += '\n_Number (' + numberCount + '):_ ' + phoneNumber;
numberCount++; numberCount++;
} } else if (key.includes('TEL')) {
if (key.includes('TEL')) {
const phoneNumber = contactInfo[key]; const phoneNumber = contactInfo[key];
formattedContact += `\n**number:** ${phoneNumber}`; formattedContact += '\n_Number (' + numberCount + '):_ ' + phoneNumber;
numberCount++; numberCount++;
} }
}); });
@ -1495,6 +1553,62 @@ export class ChatwootService {
return formattedContactsArray; return formattedContactsArray;
} }
if (typeKey === 'listMessage') {
const listTitle = result?.title || 'Unknown';
const listDescription = result?.description || 'Unknown';
const listFooter = result?.footerText || 'Unknown';
let formattedList =
'*List Menu:*\n\n' +
'_Title_: ' +
listTitle +
'\n' +
'_Description_: ' +
listDescription +
'\n' +
'_Footer_: ' +
listFooter;
if (result.sections && result.sections.length > 0) {
result.sections.forEach((section, sectionIndex) => {
formattedList += '\n\n*Section ' + (sectionIndex + 1) + ':* ' + section.title || 'Unknown\n';
if (section.rows && section.rows.length > 0) {
section.rows.forEach((row, rowIndex) => {
formattedList += '\n*Line ' + (rowIndex + 1) + ':*\n';
formattedList += '_▪ Title:_ ' + (row.title || 'Unknown') + '\n';
formattedList += '_▪ Description:_ ' + (row.description || 'Unknown') + '\n';
formattedList += '_▪ ID:_ ' + (row.rowId || 'Unknown') + '\n';
});
} else {
formattedList += '\nNo lines found in this section.\n';
}
});
} else {
formattedList += '\nNo sections found.\n';
}
return formattedList;
}
if (typeKey === 'listResponseMessage') {
const responseTitle = result?.title || 'Unknown';
const responseDescription = result?.description || 'Unknown';
const responseRowId = result?.singleSelectReply?.selectedRowId || 'Unknown';
const formattedResponseList =
'*List Response:*\n\n' +
'_Title_: ' +
responseTitle +
'\n' +
'_Description_: ' +
responseDescription +
'\n' +
'_ID_: ' +
responseRowId;
return formattedResponseList;
}
this.logger.verbose('message content: ' + result); this.logger.verbose('message content: ' + result);
return result; return result;
@ -1591,8 +1705,21 @@ export class ChatwootService {
}, },
}); });
const random = Math.random().toString(36).substring(7); let nameFile: string;
const nameFile = `${random}.${mimeTypes.extension(downloadBase64.mimetype)}`; const messageBody = body?.message[body?.messageType];
const originalFilename = messageBody?.fileName || messageBody?.message?.documentMessage?.fileName;
if (originalFilename) {
const parsedFile = path.parse(originalFilename);
if (parsedFile.name && parsedFile.ext) {
nameFile = `${parsedFile.name}-${Math.floor(Math.random() * (99 - 10 + 1) + 10)}${parsedFile.ext}`;
}
}
if (!nameFile) {
nameFile = `${Math.random().toString(36).substring(7)}.${
mimeTypes.extension(downloadBase64.mimetype) || ''
}`;
}
const fileData = Buffer.from(downloadBase64.base64, 'base64'); const fileData = Buffer.from(downloadBase64.base64, 'base64');
@ -1620,43 +1747,41 @@ export class ChatwootService {
} }
this.logger.verbose('send data to chatwoot'); this.logger.verbose('send data to chatwoot');
const send = await this.sendData(getConversation, fileName, messageType, content, instance, body); const send = await this.sendData(
getConversation,
fileName,
messageType,
content,
instance,
body,
'WAID:' + body.key.id,
);
if (!send) { if (!send) {
this.logger.warn('message not sent'); this.logger.warn('message not sent');
return; return;
} }
this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`);
this.messageCache = this.loadMessageCache();
this.messageCache.add(send.id.toString());
this.logger.verbose('save message cache');
this.saveMessageCache();
return send; return send;
} else { } else {
this.logger.verbose('message is not group'); this.logger.verbose('message is not group');
this.logger.verbose('send data to chatwoot'); this.logger.verbose('send data to chatwoot');
const send = await this.sendData(getConversation, fileName, messageType, bodyMessage, instance, body); const send = await this.sendData(
getConversation,
fileName,
messageType,
bodyMessage,
instance,
body,
'WAID:' + body.key.id,
);
if (!send) { if (!send) {
this.logger.warn('message not sent'); this.logger.warn('message not sent');
return; return;
} }
this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`);
this.messageCache = this.loadMessageCache();
this.messageCache.add(send.id.toString());
this.logger.verbose('save message cache');
this.saveMessageCache();
return send; return send;
} }
} }
@ -1675,16 +1800,12 @@ export class ChatwootService {
{ {
message: { extendedTextMessage: { contextInfo: { stanzaId: reactionMessage.key.id } } }, message: { extendedTextMessage: { contextInfo: { stanzaId: reactionMessage.key.id } } },
}, },
'WAID:' + body.key.id,
); );
if (!send) { if (!send) {
this.logger.warn('message not sent'); this.logger.warn('message not sent');
return; return;
} }
this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`);
this.messageCache = this.loadMessageCache();
this.messageCache.add(send.id.toString());
this.logger.verbose('save message cache');
this.saveMessageCache();
} }
return; return;
@ -1734,6 +1855,7 @@ export class ChatwootService {
`${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`, `${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`,
instance, instance,
body, body,
'WAID:' + body.key.id,
); );
if (!send) { if (!send) {
@ -1741,15 +1863,6 @@ export class ChatwootService {
return; return;
} }
this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`);
this.messageCache = this.loadMessageCache();
this.messageCache.add(send.id.toString());
this.logger.verbose('save message cache');
this.saveMessageCache();
return send; return send;
} }
@ -1769,43 +1882,43 @@ export class ChatwootService {
} }
this.logger.verbose('send data to chatwoot'); this.logger.verbose('send data to chatwoot');
const send = await this.createMessage(instance, getConversation, content, messageType, false, [], body); const send = await this.createMessage(
instance,
getConversation,
content,
messageType,
false,
[],
body,
'WAID:' + body.key.id,
);
if (!send) { if (!send) {
this.logger.warn('message not sent'); this.logger.warn('message not sent');
return; return;
} }
this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`);
this.messageCache = this.loadMessageCache();
this.messageCache.add(send.id.toString());
this.logger.verbose('save message cache');
this.saveMessageCache();
return send; return send;
} else { } else {
this.logger.verbose('message is not group'); this.logger.verbose('message is not group');
this.logger.verbose('send data to chatwoot'); this.logger.verbose('send data to chatwoot');
const send = await this.createMessage(instance, getConversation, bodyMessage, messageType, false, [], body); const send = await this.createMessage(
instance,
getConversation,
bodyMessage,
messageType,
false,
[],
body,
'WAID:' + body.key.id,
);
if (!send) { if (!send) {
this.logger.warn('message not sent'); this.logger.warn('message not sent');
return; return;
} }
this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`);
this.messageCache = this.loadMessageCache();
this.messageCache.add(send.id.toString());
this.logger.verbose('save message cache');
this.saveMessageCache();
return send; return send;
} }
} }
@ -1820,6 +1933,16 @@ export class ChatwootService {
const message = await this.getMessageByKeyId(instance, body.key.id); const message = await this.getMessageByKeyId(instance, body.key.id);
if (message?.chatwoot?.messageId && message?.chatwoot?.conversationId) { if (message?.chatwoot?.messageId && message?.chatwoot?.conversationId) {
this.logger.verbose('deleting message in repository. Message id: ' + body.key.id);
this.repository.message.delete({
where: {
key: {
id: body.key.id,
},
owner: instance.instanceName,
},
});
this.logger.verbose('deleting message in chatwoot. Message id: ' + body.key.id); this.logger.verbose('deleting message in chatwoot. Message id: ' + body.key.id);
return await client.messages.delete({ return await client.messages.delete({
accountId: this.provider.account_id, accountId: this.provider.account_id,
@ -1829,6 +1952,44 @@ export class ChatwootService {
} }
} }
if (event === 'messages.read') {
this.logger.verbose('read message from instance: ' + instance.instanceName);
if (!body?.key?.id || !body?.key?.remoteJid) {
this.logger.warn('message id not found');
return;
}
const message = await this.getMessageByKeyId(instance, body.key.id);
const { conversationId, contactInbox } = message?.chatwoot || {};
if (conversationId) {
let sourceId = contactInbox?.sourceId;
const inbox = (await this.getInbox(instance)) as inbox & {
inbox_identifier?: string;
};
if (!sourceId && inbox) {
const contact = (await this.findContact(
instance,
this.getNumberFromRemoteJid(body.key.remoteJid),
)) as contact;
const contactInbox = contact?.contact_inboxes?.find((contactInbox) => contactInbox?.inbox?.id === inbox.id);
sourceId = contactInbox?.source_id;
}
if (sourceId && inbox?.inbox_identifier) {
const url =
`/public/api/v1/inboxes/${inbox.inbox_identifier}/contacts/${sourceId}` +
`/conversations/${conversationId}/update_last_seen`;
chatwootRequest(this.getClientCwConfig(), {
method: 'POST',
url: url,
});
}
}
return;
}
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;
@ -1854,6 +2015,7 @@ export class ChatwootService {
const msgConnection = `🚀 Connection successfully established!`; const msgConnection = `🚀 Connection successfully established!`;
this.logger.verbose('send message to chatwoot'); this.logger.verbose('send message to chatwoot');
await this.createBotMessage(instance, msgConnection, 'incoming'); await this.createBotMessage(instance, msgConnection, 'incoming');
this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0;
} }
} }
} }
@ -1899,4 +2061,8 @@ export class ChatwootService {
this.logger.error(error); this.logger.error(error);
} }
} }
public getNumberFromRemoteJid(remoteJid: string) {
return remoteJid.replace(/:\d+/, '').split('@')[0];
}
} }

View File

@ -26,6 +26,7 @@ import {
WebsocketModel, WebsocketModel,
} from '../models'; } from '../models';
import { RepositoryBroker } from '../repository/repository.manager'; import { RepositoryBroker } from '../repository/repository.manager';
import { CacheService } from './cache.service';
import { WAStartupService } from './whatsapp.service'; import { WAStartupService } from './whatsapp.service';
import { WAStartupClass } from '../whatsapp.module'; import { WAStartupClass } from '../whatsapp.module';
@ -35,6 +36,7 @@ export class WAMonitoringService {
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly repository: RepositoryBroker, private readonly repository: RepositoryBroker,
private readonly cache: RedisCache, private readonly cache: RedisCache,
private readonly chatwootCache: CacheService,
) { ) {
this.logger.verbose('instance created'); this.logger.verbose('instance created');
@ -360,14 +362,13 @@ export class WAMonitoringService {
} }
private async setInstance(name: string) { private async setInstance(name: string) {
const path = join(INSTANCE_DIR, name); const instance = new WAStartupService(
let values: any; this.configService,
if(this.db.ENABLED ) this.eventEmitter,
values = await this.dbInstance.collection(name).findOne({ _id: 'integration' }) this.repository,
else this.cache,
values = JSON.parse(readFileSync(path + '/integration.json', 'utf8')); this.chatwootCache,
const instance = new WAStartupClass[values.integration] );
(this.configService, this.eventEmitter, this.repository, this.cache);
instance.instanceName = name; instance.instanceName = name;
instance.instanceNumber = values.number; instance.instanceNumber = values.number;
instance.instanceToken = values.token; instance.instanceToken = values.token;
@ -451,6 +452,7 @@ 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.waInstances[instanceName]?.clearCacheChatwoot();
this.logger.verbose('request cleaning up instance: ' + instanceName); this.logger.verbose('request cleaning up instance: ' + instanceName);
this.cleaningUp(instanceName); this.cleaningUp(instanceName);
} finally { } finally {

View File

@ -27,7 +27,7 @@ export class ProxyService {
return result; return result;
} catch (error) { } catch (error) {
return { enabled: false, proxy: '' }; return { enabled: false, proxy: null };
} }
} }
} }

View File

@ -389,6 +389,7 @@ export class TypebotService {
input, input,
clientSideActions, clientSideActions,
this.eventEmitter, this.eventEmitter,
applyFormatting,
).catch((err) => { ).catch((err) => {
console.error('Erro ao processar mensagens:', err); console.error('Erro ao processar mensagens:', err);
}); });
@ -403,85 +404,71 @@ export class TypebotService {
} }
return null; return null;
} }
function applyFormatting(element) {
let text = '';
async function processMessages(instance, messages, input, clientSideActions, eventEmitter) { if (element.text) {
let qtdMessages = 0, buttonText = ''; text += element.text;
}
if (element.type === 'p' || element.type === 'inline-variable' || element.type === 'a') {
for (const child of element.children) {
text += applyFormatting(child);
}
}
let formats = '';
if (element.bold) {
formats += '*';
}
if (element.italic) {
formats += '_';
}
if (element.underline) {
formats += '~';
}
let formattedText = `${formats}${text}${formats.split('').reverse().join('')}`;
if (element.url) {
formattedText = element.children[0]?.text ? `[${formattedText}]\n(${element.url})` : `${element.url}`;
}
return formattedText;
}
async function processMessages(instance, messages, input, clientSideActions, eventEmitter, applyFormatting) {
for (const message of messages) { for (const message of messages) {
const wait = findItemAndGetSecondsToWait(clientSideActions, message.id); const wait = findItemAndGetSecondsToWait(clientSideActions, message.id);
if (message.type === 'text') { if (message.type === 'text') {
let formattedText = ''; let formattedText = '';
let linkPreview = false;
for (const richText of message.content.richText) { for (const richText of message.content.richText) {
if (richText.type === 'variable') { for (const element of richText.children) {
for (const child of richText.children) { formattedText += applyFormatting(element);
for (const grandChild of child.children) {
formattedText += grandChild.text;
}
}
} else {
for (const element of richText.children) {
let text = '';
if (element.type === 'inline-variable') {
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;
}
} }
formattedText += '\n'; formattedText += '\n';
} }
formattedText = formattedText.replace(/\n$/, ''); formattedText = formattedText.replace(/\*\*/g, '').replace(/__/, '').replace(/~~/, '').replace(/\n$/, '');
qtdMessages++;
if (instance?.constructor.name == Integration.WABussinessService && await instance.textMessage({
input?.type === 'choice input' && messages.length == qtdMessages) { number: remoteJid.split('@')[0],
buttonText = formattedText; options: {
} else { delay: wait ? wait * 1000 : instance.localTypebot.delay_message || 1000,
await instance.textMessage({ presence: 'composing',
number: remoteJid.split('@')[0], },
options: { textMessage: {
delay: wait ? wait * 1000 : instance.localTypebot.delay_message || 1000, text: formattedText,
presence: 'composing', },
linkPreview: linkPreview, });
},
textMessage: {
text: formattedText,
},
});
}
} }
if (message.type === 'image') { if (message.type === 'image') {
await instance.mediaMessage({ await instance.mediaMessage({
number: remoteJid.split('@')[0], number: remoteJid.split('@')[0],
@ -570,6 +557,19 @@ export class TypebotService {
}, },
}); });
} }
formattedText = formattedText.replace(/\n$/, '');
await instance.textMessage({
number: remoteJid.split('@')[0],
options: {
delay: 1200,
presence: 'composing',
},
textMessage: {
text: formattedText,
},
});
} }
} else { } else {
eventEmitter.emit('typebot:end', { eventEmitter.emit('typebot:end', {

File diff suppressed because it is too large Load Diff

View File

@ -112,9 +112,17 @@ export declare namespace wa {
sessions?: Session[]; sessions?: Session[];
}; };
type Proxy = {
host?: string;
port?: string;
protocol?: string;
username?: string;
password?: string;
};
export type LocalProxy = { export type LocalProxy = {
enabled?: boolean; enabled?: boolean;
proxy?: string; proxy?: Proxy;
}; };
export type LocalChamaai = { export type LocalChamaai = {

View File

@ -1,6 +1,7 @@
import { configService } from '../config/env.config'; import { configService } from '../config/env.config';
import { eventEmitter } from '../config/event.config'; import { eventEmitter } from '../config/event.config';
import { Logger } from '../config/logger.config'; import { Logger } from '../config/logger.config';
import { CacheEngine } from '../libs/cacheengine';
import { dbserver } from '../libs/db.connect'; import { dbserver } from '../libs/db.connect';
import { RedisCache } from '../libs/redis.client'; import { RedisCache } from '../libs/redis.client';
import { ChamaaiController } from './controllers/chamaai.controller'; import { ChamaaiController } from './controllers/chamaai.controller';
@ -48,6 +49,7 @@ import { TypebotRepository } from './repository/typebot.repository';
import { WebhookRepository } from './repository/webhook.repository'; import { WebhookRepository } from './repository/webhook.repository';
import { WebsocketRepository } from './repository/websocket.repository'; import { WebsocketRepository } from './repository/websocket.repository';
import { AuthService } from './services/auth.service'; import { AuthService } from './services/auth.service';
import { CacheService } from './services/cache.service';
import { ChamaaiService } from './services/chamaai.service'; import { ChamaaiService } from './services/chamaai.service';
import { ChatwootService } from './services/chatwoot.service'; import { ChatwootService } from './services/chatwoot.service';
import { WAMonitoringService } from './services/monitor.service'; import { WAMonitoringService } from './services/monitor.service';
@ -102,7 +104,9 @@ export const repository = new RepositoryBroker(
export const cache = new RedisCache(); export const cache = new RedisCache();
export const waMonitor = new WAMonitoringService(eventEmitter, configService, repository, cache); const chatwootCache = new CacheService(new CacheEngine(configService, ChatwootService.name).getEngine());
export const waMonitor = new WAMonitoringService(eventEmitter, configService, repository, cache, chatwootCache);
const authService = new AuthService(configService, waMonitor, repository); const authService = new AuthService(configService, waMonitor, repository);
@ -134,7 +138,7 @@ const sqsService = new SqsService(waMonitor);
export const sqsController = new SqsController(sqsService); export const sqsController = new SqsController(sqsService);
const chatwootService = new ChatwootService(waMonitor, configService, repository); const chatwootService = new ChatwootService(waMonitor, configService, repository, chatwootCache);
export const chatwootController = new ChatwootController(chatwootService, configService, repository); export const chatwootController = new ChatwootController(chatwootService, configService, repository);
@ -153,10 +157,10 @@ export const instanceController = new InstanceController(
settingsService, settingsService,
websocketService, websocketService,
rabbitmqService, rabbitmqService,
proxyService,
sqsService, sqsService,
typebotService, typebotService,
cache, cache,
chatwootCache,
); );
export const sendMessageController = new SendMessageController(waMonitor); export const sendMessageController = new SendMessageController(waMonitor);
export const chatController = new ChatController(waMonitor); export const chatController = new ChatController(waMonitor);