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)
### Fixed

View File

@ -1,6 +1,6 @@
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 contact="contato@agenciadgcode.com"

View File

@ -1,6 +1,6 @@
{
"name": "evolution-api",
"version": "1.6.1",
"version": "1.6.2",
"description": "Rest api for communication with WhatsApp",
"main": "./dist/src/main.js",
"scripts": {
@ -46,7 +46,7 @@
"@figuro/chatwoot-sdk": "^1.1.16",
"@hapi/boom": "^10.0.1",
"@sentry/node": "^7.59.2",
"@whiskeysockets/baileys": "github:PurpShell/Baileys#combined",
"@whiskeysockets/baileys": "^6.5.0",
"amqplib": "^0.10.3",
"aws-sdk": "^2.1499.0",
"axios": "^1.3.5",

View File

@ -136,11 +136,22 @@ export type GlobalWebhook = {
ENABLED: 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 Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook };
export type ConfigSessionPhone = { CLIENT: string; NAME: string };
export type QrCode = { LIMIT: number; COLOR: string };
export type Typebot = { API_VERSION: string; KEEP_OPEN: boolean };
export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal };
export type Production = boolean;
export interface Env {
@ -160,6 +171,7 @@ export interface Env {
CONFIG_SESSION_PHONE: ConfigSessionPhone;
QRCODE: QrCode;
TYPEBOT: Typebot;
CACHE: CacheConf;
AUTHENTICATION: Auth;
PRODUCTION?: Production;
WABUSSINESS: WABussiness;
@ -326,6 +338,18 @@ export class ConfigService {
API_VERSION: process.env?.TYPEBOT_API_VERSION || 'old',
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: {
TYPE: process.env.AUTHENTICATION_TYPE as 'apikey',
API_KEY: {

View File

@ -162,6 +162,17 @@ TYPEBOT:
API_VERSION: 'old' # old | latest
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
# 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

View File

@ -25,7 +25,7 @@ info:
</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)
version: 1.6.1
version: 1.6.2
contact:
name: DavidsonGomes
email: contato@agenciadgcode.com

View File

@ -27,6 +27,7 @@ export const initAMQP = () => {
channel.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
assert: true,
});
amqpChannel = channel;
@ -43,7 +44,7 @@ export const getAMQP = (): amqp.Channel | null => {
};
export const initQueues = (instanceName: string, events: string[]) => {
if (!events || !events.length) return;
if (!instanceName || !events || !events.length) return;
const queues = events.map((event) => {
return `${event.replace(/_/g, '.').toLowerCase()}`;
@ -56,6 +57,7 @@ export const initQueues = (instanceName: string, events: string[]) => {
amqp.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
assert: true,
});
const queueName = `${instanceName}.${event}`;
@ -89,6 +91,7 @@ export const removeQueues = (instanceName: string, events: string[]) => {
amqp.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
assert: true,
});
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'),
};
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 = {
$id: v4(),
type: 'object',
@ -1127,7 +1147,18 @@ export const proxySchema: JSONSchema7 = {
type: 'object',
properties: {
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'],
...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,
ReadMessageDto,
SendPresenceDto,
UpdateMessageDto,
WhatsAppNumberDto,
NumberBusiness,
} from '../dto/chat.dto';
@ -123,4 +124,9 @@ export class ChatController {
logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance');
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 { Logger } from '../../config/logger.config';
import { BadRequestException } from '../../exceptions';
import { CacheEngine } from '../../libs/cacheengine';
import { ChatwootDto } from '../dto/chatwoot.dto';
import { InstanceDto } from '../dto/instance.dto';
import { RepositoryBroker } from '../repository/repository.manager';
import { CacheService } from '../services/cache.service';
import { ChatwootService } from '../services/chatwoot.service';
import { waMonitor } from '../whatsapp.module';
@ -94,7 +96,9 @@ export class ChatwootController {
public async receiveWebhook(instance: InstanceDto, data: any) {
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);
}

View File

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

View File

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

View File

@ -2,16 +2,30 @@ import { Schema } from 'mongoose';
import { dbserver } from '../../libs/db.connect';
class Proxy {
host?: string;
port?: string;
protocol?: string;
username?: string;
password?: string;
}
export class ProxyRaw {
_id?: string;
enabled?: boolean;
proxy?: string;
proxy?: Proxy;
}
const proxySchema = new Schema<ProxyRaw>({
_id: { type: String, _id: 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');

View File

@ -1,4 +1,4 @@
import { opendirSync, readFileSync } from 'fs';
import { opendirSync, readFileSync, rmSync } from 'fs';
import { join } from 'path';
import { ConfigService, StoreConf } from '../../config/env.config';
@ -18,6 +18,19 @@ export class MessageRepository extends Repository {
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> {
this.logger.verbose('inserting messages');
@ -91,14 +104,7 @@ export class MessageRepository extends Repository {
this.logger.verbose('finding messages');
if (this.dbSettings.ENABLED) {
this.logger.verbose('finding messages in db');
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];
}
}
query = this.buildQuery(query);
return await this.messageModel
.find({ ...query.where })
@ -198,15 +204,23 @@ export class MessageRepository extends Repository {
}
}
public async delete(query: any) {
public async delete(query: MessageQuery) {
try {
this.logger.verbose('deleting messages');
this.logger.verbose('deleting message');
if (this.dbSettings.ENABLED) {
this.logger.verbose('deleting messages in db');
return await this.messageModel.deleteMany(query);
this.logger.verbose('deleting message in db');
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) {
return { error: error?.toString() };
}

View File

@ -14,6 +14,7 @@ import {
profileSchema,
profileStatusSchema,
readMessageSchema,
updateMessageSchema,
whatsappNumberSchema,
profileBusinessSchema,
} from '../../validate/validate.schema';
@ -29,6 +30,7 @@ import {
ProfileStatusDto,
ReadMessageDto,
SendPresenceDto,
UpdateMessageDto,
WhatsAppNumberDto,
NumberBusiness,
} from '../dto/chat.dto';
@ -383,6 +385,23 @@ export class ChatRouter extends RouterBroker {
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);
});
}

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 FormData from 'form-data';
import { createReadStream, readFileSync, unlinkSync, writeFileSync } from 'fs';
import { createReadStream, unlinkSync, writeFileSync } from 'fs';
import Jimp from 'jimp';
import mimeTypes from 'mime-types';
import path from 'path';
import { ConfigService, HttpServer, WABussiness } from '../../config/env.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 { InstanceDto } from '../dto/instance.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 { Events } from '../types/wa.types';
import { WAMonitoringService } from './monitor.service';
export class ChatwootService {
private messageCacheFile: string;
private messageCache: Set<string>;
private readonly logger = new Logger(ChatwootService.name);
private provider: any;
@ -29,35 +27,15 @@ export class ChatwootService {
private readonly waMonitor: WAMonitoringService,
private readonly configService: ConfigService,
private readonly repository: RepositoryBroker,
) {
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 readonly cache: ICache,
) {}
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);
const provider = await this.waMonitor.waInstances[instance.instanceName]?.findChatwoot();
@ -68,6 +46,8 @@ export class ChatwootService {
this.logger.verbose('provider found');
this.cache.set(cacheKey, provider);
return provider;
// try {
// } catch (error) {
@ -92,12 +72,7 @@ export class ChatwootService {
this.logger.verbose('create client to instance: ' + instance.instanceName);
const client = new ChatwootClient({
config: {
basePath: provider.url,
with_credentials: true,
credentials: 'include',
token: provider.token,
},
config: this.getClientCwConfig(),
});
this.logger.verbose('client created');
@ -105,6 +80,19 @@ export class ChatwootService {
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) {
this.logger.verbose('create chatwoot: ' + instance.instanceName);
@ -419,6 +407,26 @@ export class ChatwootService {
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');
this.logger.verbose('is group: ' + isGroup);
@ -569,6 +577,7 @@ export class ChatwootService {
if (conversation) {
this.logger.verbose('conversation found');
this.cache.set(cacheKey, conversation.id);
return conversation.id;
}
}
@ -594,6 +603,7 @@ export class ChatwootService {
}
this.logger.verbose('conversation created');
this.cache.set(cacheKey, conversation.id);
return conversation.id;
} catch (error) {
this.logger.error(error);
@ -603,6 +613,11 @@ export class ChatwootService {
public async getInbox(instance: InstanceDto) {
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);
if (!client) {
@ -629,6 +644,7 @@ export class ChatwootService {
}
this.logger.verbose('return inbox');
this.cache.set(cacheKey, findByName);
return findByName;
}
@ -644,6 +660,7 @@ export class ChatwootService {
filename: string;
}[],
messageBody?: any,
sourceId?: string,
) {
this.logger.verbose('create message to instance: ' + instance.instanceName);
@ -665,6 +682,7 @@ export class ChatwootService {
message_type: messageType,
attachments: attachments,
private: privateMessage || false,
source_id: sourceId,
content_attributes: {
...replyToIds,
},
@ -765,6 +783,7 @@ export class ChatwootService {
content?: string,
instance?: InstanceDto,
messageBody?: any,
sourceId?: string,
) {
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);
const config = {
method: 'post',
@ -916,17 +939,21 @@ export class ChatwootService {
try {
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]);
this.logger.verbose('file name: ' + fileName);
if (!mimeType) {
const parts = media.split('/');
fileName = decodeURIComponent(parts[parts.length - 1]);
this.logger.verbose('file name: ' + fileName);
const response = await axios.get(media, {
responseType: 'arraybuffer',
});
const mimeType = response.headers['content-type'];
this.logger.verbose('mime type: ' + mimeType);
const response = await axios.get(media, {
responseType: 'arraybuffer',
});
mimeType = response.headers['content-type'];
this.logger.verbose('mime type: ' + mimeType);
}
let type = 'document';
@ -1008,6 +1035,17 @@ export class ChatwootService {
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');
if (
!body?.conversation ||
@ -1029,7 +1067,7 @@ export class ChatwootService {
.replaceAll(/(?<!`)`((?!\s)([^`*]+?)(?<!\s))`(?!`)/g, '```$1```') // Substitui ` por ```
: 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];
this.logger.verbose('check if is a message deletion');
@ -1044,7 +1082,18 @@ export class ChatwootService {
limit: 1,
});
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 });
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' };
}
@ -1105,22 +1154,11 @@ export class ChatwootService {
if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') {
this.logger.verbose('check if is group');
this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`);
this.logger.verbose('cache file path: ' + this.messageCacheFile);
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');
if (body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:') {
this.logger.verbose('message sent directly from whatsapp. Webhook ignored.');
return { message: 'bot' };
}
this.logger.verbose('clear cache');
this.clearMessageCache();
this.logger.verbose('Format message to send');
let formatText: string;
const regex = /^▶️.*◀️$/;
@ -1197,6 +1235,9 @@ export class ChatwootService {
messageId: body.id,
inboxId: body.inbox?.id,
conversationId: body.conversation?.id,
contactInbox: {
sourceId: body.conversation?.contact_inbox?.source_id,
},
},
instance,
);
@ -1228,6 +1269,9 @@ export class ChatwootService {
messageId: body.id,
inboxId: body.inbox?.id,
conversationId: body.conversation?.id,
contactInbox: {
sourceId: body.conversation?.contact_inbox?.source_id,
},
},
instance,
);
@ -1396,6 +1440,8 @@ export class ChatwootService {
contactsArrayMessage: msg.contactsArrayMessage,
locationMessage: msg.locationMessage,
liveLocationMessage: msg.liveLocationMessage,
listMessage: msg.listMessage,
listResponseMessage: msg.listResponseMessage,
};
this.logger.verbose('type message: ' + types);
@ -1413,11 +1459,27 @@ export class ChatwootService {
const latitude = result.degreesLatitude;
const longitude = result.degreesLongitude;
const formattedLocation = `**Location:**
**latitude:** ${latitude}
**longitude:** ${longitude}
https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}
`;
const locationName = result?.name || 'Unknown';
const locationAddress = result?.address || 'Unknown';
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);
@ -1435,19 +1497,17 @@ export class ChatwootService {
}
});
let formattedContact = `**Contact:**
**name:** ${contactInfo['FN']}`;
let formattedContact = '*Contact:*\n\n' + '_Name:_ ' + contactInfo['FN'];
let numberCount = 1;
Object.keys(contactInfo).forEach((key) => {
if (key.startsWith('item') && key.includes('TEL')) {
const phoneNumber = contactInfo[key];
formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`;
formattedContact += '\n_Number (' + numberCount + '):_ ' + phoneNumber;
numberCount++;
}
if (key.includes('TEL')) {
} else if (key.includes('TEL')) {
const phoneNumber = contactInfo[key];
formattedContact += `\n**number:** ${phoneNumber}`;
formattedContact += '\n_Number (' + numberCount + '):_ ' + phoneNumber;
numberCount++;
}
});
@ -1468,19 +1528,17 @@ export class ChatwootService {
}
});
let formattedContact = `**Contact:**
**name:** ${contact.displayName}`;
let formattedContact = '*Contact:*\n\n' + '_Name:_ ' + contact.displayName;
let numberCount = 1;
Object.keys(contactInfo).forEach((key) => {
if (key.startsWith('item') && key.includes('TEL')) {
const phoneNumber = contactInfo[key];
formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`;
formattedContact += '\n_Number (' + numberCount + '):_ ' + phoneNumber;
numberCount++;
}
if (key.includes('TEL')) {
} else if (key.includes('TEL')) {
const phoneNumber = contactInfo[key];
formattedContact += `\n**number:** ${phoneNumber}`;
formattedContact += '\n_Number (' + numberCount + '):_ ' + phoneNumber;
numberCount++;
}
});
@ -1495,6 +1553,62 @@ export class ChatwootService {
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);
return result;
@ -1591,8 +1705,21 @@ export class ChatwootService {
},
});
const random = Math.random().toString(36).substring(7);
const nameFile = `${random}.${mimeTypes.extension(downloadBase64.mimetype)}`;
let nameFile: string;
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');
@ -1620,43 +1747,41 @@ export class ChatwootService {
}
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) {
this.logger.warn('message not sent');
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;
} else {
this.logger.verbose('message is not group');
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) {
this.logger.warn('message not sent');
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;
}
}
@ -1675,16 +1800,12 @@ export class ChatwootService {
{
message: { extendedTextMessage: { contextInfo: { stanzaId: reactionMessage.key.id } } },
},
'WAID:' + body.key.id,
);
if (!send) {
this.logger.warn('message not sent');
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;
@ -1734,6 +1855,7 @@ export class ChatwootService {
`${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`,
instance,
body,
'WAID:' + body.key.id,
);
if (!send) {
@ -1741,15 +1863,6 @@ export class ChatwootService {
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;
}
@ -1769,43 +1882,43 @@ export class ChatwootService {
}
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) {
this.logger.warn('message not sent');
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;
} else {
this.logger.verbose('message is not group');
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) {
this.logger.warn('message not sent');
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;
}
}
@ -1820,6 +1933,16 @@ export class ChatwootService {
const message = await this.getMessageByKeyId(instance, body.key.id);
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);
return await client.messages.delete({
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') {
this.logger.verbose('event status.instance');
const data = body;
@ -1854,6 +2015,7 @@ export class ChatwootService {
const msgConnection = `🚀 Connection successfully established!`;
this.logger.verbose('send message to chatwoot');
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);
}
}
public getNumberFromRemoteJid(remoteJid: string) {
return remoteJid.replace(/:\d+/, '').split('@')[0];
}
}

View File

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

View File

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

View File

@ -389,6 +389,7 @@ export class TypebotService {
input,
clientSideActions,
this.eventEmitter,
applyFormatting,
).catch((err) => {
console.error('Erro ao processar mensagens:', err);
});
@ -403,85 +404,71 @@ export class TypebotService {
}
return null;
}
function applyFormatting(element) {
let text = '';
async function processMessages(instance, messages, input, clientSideActions, eventEmitter) {
let qtdMessages = 0, buttonText = '';
if (element.text) {
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) {
const wait = findItemAndGetSecondsToWait(clientSideActions, message.id);
if (message.type === 'text') {
let formattedText = '';
let linkPreview = false;
for (const richText of message.content.richText) {
if (richText.type === 'variable') {
for (const child of richText.children) {
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;
}
for (const element of richText.children) {
formattedText += applyFormatting(element);
}
formattedText += '\n';
}
formattedText = formattedText.replace(/\n$/, '');
qtdMessages++;
if (instance?.constructor.name == Integration.WABussinessService &&
input?.type === 'choice input' && messages.length == qtdMessages) {
buttonText = formattedText;
} else {
await instance.textMessage({
number: remoteJid.split('@')[0],
options: {
delay: wait ? wait * 1000 : instance.localTypebot.delay_message || 1000,
presence: 'composing',
linkPreview: linkPreview,
},
textMessage: {
text: formattedText,
},
});
}
formattedText = formattedText.replace(/\*\*/g, '').replace(/__/, '').replace(/~~/, '').replace(/\n$/, '');
await instance.textMessage({
number: remoteJid.split('@')[0],
options: {
delay: wait ? wait * 1000 : instance.localTypebot.delay_message || 1000,
presence: 'composing',
},
textMessage: {
text: formattedText,
},
});
}
if (message.type === 'image') {
await instance.mediaMessage({
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 {
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[];
};
type Proxy = {
host?: string;
port?: string;
protocol?: string;
username?: string;
password?: string;
};
export type LocalProxy = {
enabled?: boolean;
proxy?: string;
proxy?: Proxy;
};
export type LocalChamaai = {

View File

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