diff --git a/.DS_Store b/.DS_Store index 9880dac3..0ae7ca6c 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.vscode/settings.json b/.vscode/settings.json index 5bc52817..71db0b08 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,9 @@ "editor.smoothScrolling": true, "editor.tabSize": 2, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, - "source.fixAll": true - } + "source.fixAll.eslint": "explicit", + "source.fixAll": "explicit" + }, + "prisma-smart-formatter.typescript.defaultFormatter": "esbenp.prettier-vscode", + "prisma-smart-formatter.prisma.defaultFormatter": "Prisma.prisma" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 98783e84..1b67354e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +# 1.6.1 (develop) + +### Fixed + +* Fixed Lid Messages + +# 1.6.0 (2023-12-12 17:24) + +### Feature +* Added AWS SQS Integration +* Added support for new typebot API +* Added endpoint sendPresence +* New Instance Manager +* Added auto_create to the chatwoot set to create the inbox automatically or not +* Added reply, delete and message reaction in chatwoot v3.3.1 + +### Fixed + +* Adjusts in proxy +* Adjusts in start session for Typebot +* Added mimetype field when sending media +* Ajusts in validations to messages.upsert +* Fixed messages not received: error handling when updating contact in chatwoot +* Fix workaround to manage param data as an array in mongodb +* Removed await from webhook when sending a message +* Update typebot.service.ts - element.underline change ~ for * +* Adjusts in proxy +* Removed api restart on receiving an error +* Fixes in mongodb and chatwoot +* Adjusted return from queries in mongodb +* Added restart instance when update profile picture +* Correction of chatwoot functioning with admin flows +* Fixed problem that did not generate qrcode with the chatwoot_conversation_pending option enabled +* Fixed issue where CSAT opened a new ticket when reopen_conversation was disabled +* Fixed issue sending contact to Chatwoot via iOS + +### Integrations + +- Chatwoot: v3.3.1 +- Typebot: v2.20.0 + # 1.5.4 (2023-10-09 20:43) ### Fixed diff --git a/Docker/.env.example b/Docker/.env.example index 88be79fe..fefd9456 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -51,6 +51,12 @@ RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672 WEBSOCKET_ENABLED=false +SQS_ENABLED=false +SQS_ACCESS_KEY_ID= +SQS_SECRET_ACCESS_KEY= +SQS_ACCOUNT_ID= +SQS_REGION= + # Global Webhook Settings # Each instance's Webhook URL and events will be requested at the time it is created ## Define a global webhook that will listen for enabled events from all instances @@ -99,6 +105,9 @@ CONFIG_SESSION_PHONE_NAME=Chrome QRCODE_LIMIT=30 QRCODE_COLOR=#198754 +# old | latest +TYPEBOT_API_VERSION=latest + # 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 diff --git a/Dockerfile b/Dockerfile index dee414d6..10be07c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM node:20.7.0-alpine -LABEL version="1.5.4" description="Api to control whatsapp features through http requests." +LABEL version="1.6.0" description="Api to control whatsapp features through http requests." LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" LABEL contact="contato@agenciadgcode.com" @@ -56,6 +56,12 @@ ENV RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672 ENV WEBSOCKET_ENABLED=false +ENV SQS_ENABLED=false +ENV SQS_ACCESS_KEY_ID= +ENV SQS_SECRET_ACCESS_KEY= +ENV SQS_ACCOUNT_ID= +ENV SQS_REGION= + ENV WEBHOOK_GLOBAL_URL= ENV WEBHOOK_GLOBAL_ENABLED=false @@ -98,6 +104,8 @@ ENV CONFIG_SESSION_PHONE_NAME=Chrome ENV QRCODE_LIMIT=30 ENV QRCODE_COLOR=#198754 +ENV TYPEBOT_API_VERSION=latest + ENV AUTHENTICATION_TYPE=apikey ENV AUTHENTICATION_API_KEY=B6D711FCDE4D4FD5936544120E713976 diff --git a/README.md b/README.md index c5c4bc37..13e68151 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,13 @@ This code was produced based on the baileys library and it is still under develo -#### Buy me coffe +#### Buy me coffe - PIX
- + +

CHAVE PIX (Telefone): (74)99987-9409


\ No newline at end of file diff --git a/package.json b/package.json index e00f1e39..19891113 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.5.4", + "version": "1.6.0", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { @@ -48,6 +48,7 @@ "@sentry/node": "^7.59.2", "@whiskeysockets/baileys": "^6.5.0", "amqplib": "^0.10.3", + "aws-sdk": "^2.1499.0", "axios": "^1.3.5", "class-validator": "^0.13.2", "compression": "^1.7.4", @@ -55,6 +56,7 @@ "cross-env": "^7.0.3", "dayjs": "^1.11.7", "eventemitter2": "^6.4.9", + "evolution-manager": "^0.4.4", "exiftool-vendored": "^22.0.0", "express": "^4.18.2", "express-async-errors": "^3.1.1", diff --git a/public/images/qrcode-pix.png b/public/images/qrcode-pix.png new file mode 100644 index 00000000..36ef00c6 Binary files /dev/null and b/public/images/qrcode-pix.png differ diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 873c54ce..da491505 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -66,12 +66,16 @@ export type Rabbitmq = { URI: string; }; -export type Websocket = { +export type Sqs = { ENABLED: boolean; + ACCESS_KEY_ID: string; + SECRET_ACCESS_KEY: string; + ACCOUNT_ID: string; + REGION: string; }; -export type Chatwoot = { - USE_REPLY_ID: boolean; +export type Websocket = { + ENABLED: boolean; }; export type EventsWebhook = { @@ -124,6 +128,7 @@ 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 }; export type Production = boolean; export interface Env { @@ -135,15 +140,16 @@ export interface Env { DATABASE: Database; REDIS: Redis; RABBITMQ: Rabbitmq; + SQS: Sqs; WEBSOCKET: Websocket; LOG: Log; DEL_INSTANCE: DelInstance; WEBHOOK: Webhook; CONFIG_SESSION_PHONE: ConfigSessionPhone; QRCODE: QrCode; + TYPEBOT: Typebot; AUTHENTICATION: Auth; PRODUCTION?: Production; - CHATWOOT?: Chatwoot; } export type Key = keyof Env; @@ -226,6 +232,13 @@ export class ConfigService { ENABLED: process.env?.RABBITMQ_ENABLED === 'true', URI: process.env.RABBITMQ_URI || '', }, + SQS: { + ENABLED: process.env?.SQS_ENABLED === 'true', + ACCESS_KEY_ID: process.env.SQS_ACCESS_KEY_ID || '', + SECRET_ACCESS_KEY: process.env.SQS_SECRET_ACCESS_KEY || '', + ACCOUNT_ID: process.env.SQS_ACCOUNT_ID || '', + REGION: process.env.SQS_REGION || '', + }, WEBSOCKET: { ENABLED: process.env?.WEBSOCKET_ENABLED === 'true', }, @@ -289,6 +302,9 @@ export class ConfigService { LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, COLOR: process.env.QRCODE_COLOR || '#198754', }, + TYPEBOT: { + API_VERSION: process.env?.TYPEBOT_API_VERSION || 'old', + }, AUTHENTICATION: { TYPE: process.env.AUTHENTICATION_TYPE as 'apikey', API_KEY: { @@ -302,9 +318,6 @@ export class ConfigService { SECRET: process.env.AUTHENTICATION_JWT_SECRET || 'L=0YWt]b2w[WF>#>:&E`', }, }, - CHATWOOT: { - USE_REPLY_ID: process.env?.USE_REPLY_ID === 'true', - }, }; } } diff --git a/src/config/error.config.ts b/src/config/error.config.ts index 7a6717da..6449d52e 100644 --- a/src/config/error.config.ts +++ b/src/config/error.config.ts @@ -8,7 +8,6 @@ export function onUnexpectedError() { stderr: process.stderr.fd, error, }); - process.exit(1); }); process.on('unhandledRejection', (error, origin) => { @@ -18,6 +17,5 @@ export function onUnexpectedError() { stderr: process.stderr.fd, error, }); - process.exit(1); }); } diff --git a/src/dev-env.yml b/src/dev-env.yml index 7af78d40..117226a2 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -83,6 +83,13 @@ RABBITMQ: ENABLED: false URI: "amqp://guest:guest@localhost:5672" +SQS: + ENABLED: true + ACCESS_KEY_ID: "" + SECRET_ACCESS_KEY: "" + ACCOUNT_ID: "" + REGION: "us-east-1" + WEBSOCKET: ENABLED: false @@ -139,6 +146,9 @@ QRCODE: LIMIT: 30 COLOR: "#198754" +TYPEBOT: + API_VERSION: 'old' # old | latest + # 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 @@ -154,7 +164,3 @@ AUTHENTICATION: JWT: EXPIRIN_IN: 0 # seconds - 3600s === 1h | zero (0) - never expires SECRET: L=0YWt]b2w[WF>#>:&E` - -# Configure to chatwoot -CHATWOOT: - USE_REPLY_ID: false diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 5f9235ac..589ba8dc 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -25,7 +25,7 @@ info: [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/26869335-5546d063-156b-4529-915f-909dd628c090?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D26869335-5546d063-156b-4529-915f-909dd628c090%26entityType%3Dcollection%26workspaceId%3D339a4ee7-378b-45c9-b5b8-fd2c0a9c2442) - version: 1.5.4 + version: 1.5.5 contact: name: DavidsonGomes email: contato@agenciadgcode.com diff --git a/src/libs/redis.client.ts b/src/libs/redis.client.ts index f03513ba..1d74ff15 100644 --- a/src/libs/redis.client.ts +++ b/src/libs/redis.client.ts @@ -5,49 +5,55 @@ import { Redis } from '../config/env.config'; import { Logger } from '../config/logger.config'; export class RedisCache { - async disconnect() { - await this.client.disconnect(); - this.statusConnection = false; - } - constructor() { - this.logger.verbose('instance created'); - process.on('beforeExit', async () => { - this.logger.verbose('instance destroyed'); - if (this.statusConnection) { - this.logger.verbose('instance disconnect'); - await this.client.disconnect(); - } - }); - } - + private readonly logger = new Logger(RedisCache.name); + private client: RedisClientType; private statusConnection = false; private instanceName: string; private redisEnv: Redis; + constructor() { + this.logger.verbose('RedisCache instance created'); + process.on('beforeExit', () => { + this.logger.verbose('RedisCache instance destroyed'); + this.disconnect(); + }); + } + public set reference(reference: string) { this.logger.verbose('set reference: ' + reference); this.instanceName = reference; } public async connect(redisEnv: Redis) { - this.logger.verbose('connecting'); + this.logger.verbose('Connecting to Redis...'); this.client = createClient({ url: redisEnv.URI }); - this.logger.verbose('connected in ' + redisEnv.URI); + this.client.on('error', (err) => this.logger.error('Redis Client Error ' + err)); + await this.client.connect(); this.statusConnection = true; this.redisEnv = redisEnv; + this.logger.verbose(`Connected to ${redisEnv.URI}`); } - private readonly logger = new Logger(RedisCache.name); - private client: RedisClientType; + public async disconnect() { + if (this.statusConnection) { + await this.client.disconnect(); + this.statusConnection = false; + this.logger.verbose('Redis client disconnected'); + } + } public async instanceKeys(): Promise { + const keys: string[] = []; try { - this.logger.verbose('instance keys: ' + this.redisEnv.PREFIX_KEY + ':*'); - return await this.client.sendCommand(['keys', this.redisEnv.PREFIX_KEY + ':*']); + this.logger.verbose('Fetching instance keys'); + for await (const key of this.client.scanIterator({ MATCH: `${this.redisEnv.PREFIX_KEY}:*` })) { + keys.push(key); + } } catch (error) { - this.logger.error(error); + this.logger.error('Error fetching instance keys ' + error); } + return keys; } public async keyExists(key?: string) { diff --git a/src/libs/sqs.server.ts b/src/libs/sqs.server.ts new file mode 100644 index 00000000..04184542 --- /dev/null +++ b/src/libs/sqs.server.ts @@ -0,0 +1,97 @@ +import { SQS } from 'aws-sdk'; + +import { configService, Sqs } from '../config/env.config'; +import { Logger } from '../config/logger.config'; + +const logger = new Logger('SQS'); + +let sqs: SQS; + +export const initSQS = () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return new Promise((resolve, reject) => { + const awsConfig = configService.get('SQS'); + sqs = new SQS({ + accessKeyId: awsConfig.ACCESS_KEY_ID, + secretAccessKey: awsConfig.SECRET_ACCESS_KEY, + region: awsConfig.REGION, + }); + + logger.info('SQS initialized'); + resolve(); + }); +}; + +export const getSQS = (): SQS => { + return sqs; +}; + +export const initQueues = (instanceName: string, events: string[]) => { + if (!events || !events.length) return; + + const queues = events.map((event) => { + return `${event.replace(/_/g, '_').toLowerCase()}`; + }); + + const sqs = getSQS(); + + queues.forEach((event) => { + const queueName = `${instanceName}_${event}.fifo`; + + sqs.createQueue( + { + QueueName: queueName, + Attributes: { + FifoQueue: 'true', + }, + }, + (err, data) => { + if (err) { + logger.error(`Error creating queue ${queueName}: ${err.message}`); + } else { + logger.info(`Queue ${queueName} created: ${data.QueueUrl}`); + } + }, + ); + }); +}; + +export const removeQueues = (instanceName: string, events: string[]) => { + if (!events || !events.length) return; + + const sqs = getSQS(); + + const queues = events.map((event) => { + return `${event.replace(/_/g, '_').toLowerCase()}`; + }); + + queues.forEach((event) => { + const queueName = `${instanceName}_${event}.fifo`; + + sqs.getQueueUrl( + { + QueueName: queueName, + }, + (err, data) => { + if (err) { + logger.error(`Error getting queue URL for ${queueName}: ${err.message}`); + } else { + const queueUrl = data.QueueUrl; + + sqs.deleteQueue( + { + QueueUrl: queueUrl, + }, + (deleteErr) => { + if (deleteErr) { + logger.error(`Error deleting queue ${queueName}: ${deleteErr.message}`); + } else { + logger.info(`Queue ${queueName} deleted`); + } + }, + ); + } + }, + ); + }); +}; diff --git a/src/main.ts b/src/main.ts index 3dcd0f2d..52bdd798 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,13 +6,14 @@ import cors from 'cors'; import express, { json, NextFunction, Request, Response, urlencoded } from 'express'; import { join } from 'path'; -import { Auth, configService, Cors, HttpServer, Rabbitmq, Webhook } from './config/env.config'; +import { Auth, configService, Cors, HttpServer, Rabbitmq, Sqs, Webhook } from './config/env.config'; import { onUnexpectedError } from './config/error.config'; import { Logger } from './config/logger.config'; import { ROOT_DIR } from './config/path.config'; import { swaggerRouter } from './docs/swagger.conf'; import { initAMQP } from './libs/amqp.server'; import { initIO } from './libs/socket.server'; +import { initSQS } from './libs/sqs.server'; import { ServerUP } from './utils/server-up'; import { HttpStatus, router } from './whatsapp/routers/index.router'; import { waMonitor } from './whatsapp/whatsapp.module'; @@ -128,6 +129,8 @@ function bootstrap() { if (configService.get('RABBITMQ')?.ENABLED) initAMQP(); + if (configService.get('SQS')?.ENABLED) initSQS(); + onUnexpectedError(); } diff --git a/src/utils/use-multi-file-auth-state-db.ts b/src/utils/use-multi-file-auth-state-db.ts index a021372a..995ac92a 100644 --- a/src/utils/use-multi-file-auth-state-db.ts +++ b/src/utils/use-multi-file-auth-state-db.ts @@ -25,7 +25,14 @@ export async function useMultiFileAuthStateDb( const writeData = async (data: any, key: string): Promise => { try { await client.connect(); - return await collection.replaceOne({ _id: key }, JSON.parse(JSON.stringify(data, BufferJSON.replacer)), { + let msgParsed = JSON.parse(JSON.stringify(data, BufferJSON.replacer)); + if (Array.isArray(msgParsed)) { + msgParsed = { + _id: key, + content_array: msgParsed, + }; + } + return await collection.replaceOne({ _id: key }, msgParsed, { upsert: true, }); } catch (error) { @@ -36,7 +43,10 @@ export async function useMultiFileAuthStateDb( const readData = async (key: string): Promise => { try { await client.connect(); - const data = await collection.findOne({ _id: key }); + let data = (await collection.findOne({ _id: key })) as any; + if (data?.content_array) { + data = data.content_array; + } const creds = JSON.stringify(data); return JSON.parse(creds, BufferJSON.reviver); } catch (error) { @@ -91,7 +101,7 @@ export async function useMultiFileAuthStateDb( }, }, saveCreds: async () => { - return writeData(creds, 'creds'); + return await writeData(creds, 'creds'); }, }; } diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index b8a4c0ad..a468b151 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -149,6 +149,16 @@ export const textMessageSchema: JSONSchema7 = { required: ['textMessage', 'number'], }; +export const presenceSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema, required: ['presence', 'delay'] }, + }, + required: ['options', 'number'], +}; + export const pollMessageSchema: JSONSchema7 = { $id: v4(), type: 'object', @@ -881,6 +891,7 @@ export const chatwootSchema: JSONSchema7 = { sign_msg: { type: 'boolean', enum: [true, false] }, reopen_conversation: { type: 'boolean', enum: [true, false] }, conversation_pending: { type: 'boolean', enum: [true, false] }, + auto_create: { type: 'boolean', enum: [true, false] }, }, required: ['enabled', 'account_id', 'token', 'url', 'sign_msg', 'reopen_conversation', 'conversation_pending'], ...isNotEmpty('account_id', 'token', 'url', 'sign_msg', 'reopen_conversation', 'conversation_pending'), @@ -987,6 +998,49 @@ export const rabbitmqSchema: JSONSchema7 = { ...isNotEmpty('enabled'), }; +export const sqsSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + enabled: { type: 'boolean', enum: [true, false] }, + events: { + type: 'array', + minItems: 0, + items: { + type: 'string', + enum: [ + 'APPLICATION_STARTUP', + 'QRCODE_UPDATED', + 'MESSAGES_SET', + 'MESSAGES_UPSERT', + 'MESSAGES_UPDATE', + 'MESSAGES_DELETE', + 'SEND_MESSAGE', + 'CONTACTS_SET', + 'CONTACTS_UPSERT', + 'CONTACTS_UPDATE', + 'PRESENCE_UPDATE', + 'CHATS_SET', + 'CHATS_UPSERT', + 'CHATS_UPDATE', + 'CHATS_DELETE', + 'GROUPS_UPSERT', + 'GROUP_UPDATE', + 'GROUP_PARTICIPANTS_UPDATE', + 'CONNECTION_UPDATE', + 'CALL', + 'NEW_JWT_TOKEN', + 'TYPEBOT_START', + 'TYPEBOT_CHANGE_STATUS', + 'CHAMA_AI_ACTION', + ], + }, + }, + }, + required: ['enabled'], + ...isNotEmpty('enabled'), +}; + export const typebotSchema: JSONSchema7 = { $id: v4(), type: 'object', diff --git a/src/whatsapp/controllers/chat.controller.ts b/src/whatsapp/controllers/chat.controller.ts index 0299841c..60a9c618 100644 --- a/src/whatsapp/controllers/chat.controller.ts +++ b/src/whatsapp/controllers/chat.controller.ts @@ -9,6 +9,7 @@ import { ProfilePictureDto, ProfileStatusDto, ReadMessageDto, + SendPresenceDto, WhatsAppNumberDto, } from '../dto/chat.dto'; import { InstanceDto } from '../dto/instance.dto'; @@ -77,6 +78,11 @@ export class ChatController { return await this.waMonitor.waInstances[instanceName].fetchChats(); } + public async sendPresence({ instanceName }: InstanceDto, data: SendPresenceDto) { + logger.verbose('requested sendPresence from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].sendPresence(data); + } + public async fetchPrivacySettings({ instanceName }: InstanceDto) { logger.verbose('requested fetchPrivacySettings from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].fetchPrivacySettings(); diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index 46b93aee..b83b2ddc 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -5,13 +5,18 @@ import { Logger } from '../../config/logger.config'; import { BadRequestException } from '../../exceptions'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; +import { RepositoryBroker } from '../repository/repository.manager'; import { ChatwootService } from '../services/chatwoot.service'; import { waMonitor } from '../whatsapp.module'; const logger = new Logger('ChatwootController'); export class ChatwootController { - constructor(private readonly chatwootService: ChatwootService, private readonly configService: ConfigService) {} + constructor( + private readonly chatwootService: ChatwootService, + private readonly configService: ConfigService, + private readonly repository: RepositoryBroker, + ) {} public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { logger.verbose('requested createChatwoot from ' + instance.instanceName + ' instance'); @@ -42,11 +47,12 @@ export class ChatwootController { data.sign_msg = false; data.reopen_conversation = false; data.conversation_pending = false; + data.auto_create = false; } data.name_inbox = instance.instanceName; - const result = this.chatwootService.create(instance, data); + const result = await this.chatwootService.create(instance, data); const urlServer = this.configService.get('SERVER').URL; @@ -64,7 +70,7 @@ export class ChatwootController { const urlServer = this.configService.get('SERVER').URL; - if (Object.keys(result).length === 0) { + if (Object.keys(result || {}).length === 0) { return { enabled: false, url: '', @@ -86,7 +92,7 @@ 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); + const chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); return chatwootService.receiveWebhook(instance, data); } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 3ead8690..8480c675 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -11,8 +11,10 @@ import { RepositoryBroker } from '../repository/repository.manager'; import { AuthService, OldToken } from '../services/auth.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'; import { TypebotService } from '../services/typebot.service'; import { WebhookService } from '../services/webhook.service'; import { WebsocketService } from '../services/websocket.service'; @@ -31,6 +33,8 @@ 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, ) {} @@ -62,6 +66,8 @@ export class InstanceController { websocket_events, rabbitmq_enabled, rabbitmq_events, + sqs_enabled, + sqs_events, typebot_url, typebot, typebot_expire, @@ -69,6 +75,7 @@ export class InstanceController { typebot_delay_message, typebot_unknown_message, typebot_listening_from_me, + proxy, }: InstanceDto) { try { this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); @@ -243,6 +250,69 @@ 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) { + this.logger.verbose('creating sqs'); + try { + let newEvents: string[] = []; + if (sqs_events.length === 0) { + newEvents = [ + 'APPLICATION_STARTUP', + 'QRCODE_UPDATED', + 'MESSAGES_SET', + 'MESSAGES_UPSERT', + 'MESSAGES_UPDATE', + 'MESSAGES_DELETE', + 'SEND_MESSAGE', + 'CONTACTS_SET', + 'CONTACTS_UPSERT', + 'CONTACTS_UPDATE', + 'PRESENCE_UPDATE', + 'CHATS_SET', + 'CHATS_UPSERT', + 'CHATS_UPDATE', + 'CHATS_DELETE', + 'GROUPS_UPSERT', + 'GROUP_UPDATE', + 'GROUP_PARTICIPANTS_UPDATE', + 'CONNECTION_UPDATE', + 'CALL', + 'NEW_JWT_TOKEN', + 'TYPEBOT_START', + 'TYPEBOT_CHANGE_STATUS', + 'CHAMA_AI_ACTION', + ]; + } else { + newEvents = sqs_events; + } + this.sqsService.create(instance, { + enabled: true, + events: newEvents, + }); + + sqsEvents = (await this.sqsService.find(instance)).events; + } catch (error) { + this.logger.log(error); + } + } + if (typebot_url) { try { if (!isURL(typebot_url, { require_tld: false })) { @@ -270,7 +340,7 @@ export class InstanceController { const settings: wa.LocalSettings = { reject_call: reject_call || false, msg_call: msg_call || '', - groups_ignore: groups_ignore || false, + groups_ignore: groups_ignore || true, always_online: always_online || false, read_messages: read_messages || false, read_status: read_status || false, @@ -310,6 +380,10 @@ export class InstanceController { enabled: rabbitmq_enabled, events: rabbitmqEvents, }, + sqs: { + enabled: sqs_enabled, + events: sqsEvents, + }, typebot: { enabled: typebot_url ? true : false, url: typebot_url, @@ -322,6 +396,7 @@ export class InstanceController { }, settings, qrcode: getQrcode, + proxy, }; this.logger.verbose('instance created'); @@ -371,15 +446,8 @@ export class InstanceController { number, reopen_conversation: chatwoot_reopen_conversation || false, conversation_pending: chatwoot_conversation_pending || false, + auto_create: true, }); - - this.chatwootService.initInstanceChatwoot( - instance, - instance.instanceName.split('-cwId-')[0], - `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, - qrcode, - number, - ); } catch (error) { this.logger.log(error); } @@ -404,6 +472,10 @@ export class InstanceController { enabled: rabbitmq_enabled, events: rabbitmqEvents, }, + sqs: { + enabled: sqs_enabled, + events: sqsEvents, + }, typebot: { enabled: typebot_url ? true : false, url: typebot_url, @@ -427,6 +499,7 @@ export class InstanceController { name_inbox: instance.instanceName, webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, }, + proxy, }; } catch (error) { this.logger.error(error.message[0]); @@ -553,15 +626,13 @@ export class InstanceController { this.logger.verbose('logging out instance: ' + instanceName); await this.logout({ instanceName }); - delete this.waMonitor.waInstances[instanceName]; - return { status: 'SUCCESS', error: false, response: { message: 'Instance deleted' } }; - } else { - this.logger.verbose('deleting instance: ' + instanceName); - - delete this.waMonitor.waInstances[instanceName]; - this.eventEmitter.emit('remove.instance', instanceName, 'inner'); - return { status: 'SUCCESS', error: false, response: { message: 'Instance deleted' } }; } + + this.logger.verbose('deleting instance: ' + instanceName); + + delete this.waMonitor.waInstances[instanceName]; + this.eventEmitter.emit('remove.instance', instanceName, 'inner'); + return { status: 'SUCCESS', error: false, response: { message: 'Instance deleted' } }; } catch (error) { throw new BadRequestException(error.toString()); } diff --git a/src/whatsapp/controllers/settings.controller.ts b/src/whatsapp/controllers/settings.controller.ts index 1a8baafc..0f559d1b 100644 --- a/src/whatsapp/controllers/settings.controller.ts +++ b/src/whatsapp/controllers/settings.controller.ts @@ -19,6 +19,7 @@ export class SettingsController { public async findSettings(instance: InstanceDto) { logger.verbose('requested findSettings from ' + instance.instanceName + ' instance'); - return this.settingsService.find(instance); + const settings = this.settingsService.find(instance); + return settings; } } diff --git a/src/whatsapp/controllers/sqs.controller.ts b/src/whatsapp/controllers/sqs.controller.ts new file mode 100644 index 00000000..063e29ed --- /dev/null +++ b/src/whatsapp/controllers/sqs.controller.ts @@ -0,0 +1,56 @@ +import { Logger } from '../../config/logger.config'; +import { InstanceDto } from '../dto/instance.dto'; +import { SqsDto } from '../dto/sqs.dto'; +import { SqsService } from '../services/sqs.service'; + +const logger = new Logger('SqsController'); + +export class SqsController { + constructor(private readonly sqsService: SqsService) {} + + public async createSqs(instance: InstanceDto, data: SqsDto) { + logger.verbose('requested createSqs from ' + instance.instanceName + ' instance'); + + if (!data.enabled) { + logger.verbose('sqs disabled'); + data.events = []; + } + + if (data.events.length === 0) { + logger.verbose('sqs events empty'); + data.events = [ + 'APPLICATION_STARTUP', + 'QRCODE_UPDATED', + 'MESSAGES_SET', + 'MESSAGES_UPSERT', + 'MESSAGES_UPDATE', + 'MESSAGES_DELETE', + 'SEND_MESSAGE', + 'CONTACTS_SET', + 'CONTACTS_UPSERT', + 'CONTACTS_UPDATE', + 'PRESENCE_UPDATE', + 'CHATS_SET', + 'CHATS_UPSERT', + 'CHATS_UPDATE', + 'CHATS_DELETE', + 'GROUPS_UPSERT', + 'GROUP_UPDATE', + 'GROUP_PARTICIPANTS_UPDATE', + 'CONNECTION_UPDATE', + 'CALL', + 'NEW_JWT_TOKEN', + 'TYPEBOT_START', + 'TYPEBOT_CHANGE_STATUS', + 'CHAMA_AI_ACTION', + ]; + } + + return this.sqsService.create(instance, data); + } + + public async findSqs(instance: InstanceDto) { + logger.verbose('requested findSqs from ' + instance.instanceName + ' instance'); + return this.sqsService.find(instance); + } +} diff --git a/src/whatsapp/controllers/views.controller.ts b/src/whatsapp/controllers/views.controller.ts deleted file mode 100644 index 7e15dfe7..00000000 --- a/src/whatsapp/controllers/views.controller.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Request, Response } from 'express'; - -import { Auth, ConfigService, HttpServer } from '../../config/env.config'; -import { HttpStatus } from '../routers/index.router'; -import { WAMonitoringService } from '../services/monitor.service'; - -export class ViewsController { - constructor(private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService) {} - - public async manager(request: Request, response: Response) { - try { - const token = this.configService.get('AUTHENTICATION').API_KEY.KEY; - const port = this.configService.get('SERVER').PORT; - - const instances = await this.waMonitor.instanceInfo(); - - console.log('INSTANCES: ', instances); - return response.status(HttpStatus.OK).render('manager', { token, port, instances }); - } catch (error) { - console.log('ERROR: ', error); - } - } -} diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index f8a5da5f..07553c90 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -1,4 +1,4 @@ -import { proto, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys'; +import { proto, WAPresence, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys'; export class OnWhatsAppDto { constructor(public readonly jid: string, public readonly exists: boolean, public readonly name?: string) {} @@ -83,3 +83,20 @@ export class DeleteMessage { remoteJid: string; participant?: string; } +export class Options { + delay?: number; + presence?: WAPresence; +} +class OptionsMessage { + options: Options; +} +export class Metadata extends OptionsMessage { + number: string; +} + +export class SendPresenceDto extends Metadata { + options: { + presence: WAPresence; + delay: number; + }; +} diff --git a/src/whatsapp/dto/chatwoot.dto.ts b/src/whatsapp/dto/chatwoot.dto.ts index b270c869..22085faf 100644 --- a/src/whatsapp/dto/chatwoot.dto.ts +++ b/src/whatsapp/dto/chatwoot.dto.ts @@ -8,4 +8,5 @@ export class ChatwootDto { number?: string; reopen_conversation?: boolean; conversation_pending?: boolean; + auto_create?: boolean; } diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index 700fa099..c63620c5 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -23,6 +23,8 @@ export class InstanceDto { websocket_events?: string[]; rabbitmq_enabled?: boolean; rabbitmq_events?: string[]; + sqs_enabled?: boolean; + sqs_events?: string[]; typebot_url?: string; typebot?: string; typebot_expire?: number; @@ -30,6 +32,5 @@ export class InstanceDto { typebot_delay_message?: number; typebot_unknown_message?: string; typebot_listening_from_me?: boolean; - proxy_enabled?: boolean; - proxy_proxy?: string; + proxy?: string; } diff --git a/src/whatsapp/dto/sendMessage.dto.ts b/src/whatsapp/dto/sendMessage.dto.ts index c2ddb3a2..bfa5763f 100644 --- a/src/whatsapp/dto/sendMessage.dto.ts +++ b/src/whatsapp/dto/sendMessage.dto.ts @@ -46,9 +46,13 @@ class PollMessage { values: string[]; messageSecret?: Uint8Array; } + export class SendTextDto extends Metadata { textMessage: TextMessage; } +export class SendPresence extends Metadata { + textMessage: TextMessage; +} export class SendStatusDto extends Metadata { statusMessage: StatusMessage; @@ -61,6 +65,7 @@ export class SendPollDto extends Metadata { export type MediaType = 'image' | 'document' | 'video' | 'audio'; export class MediaMessage { mediatype: MediaType; + mimetype?: string; caption?: string; // for document fileName?: string; diff --git a/src/whatsapp/dto/sqs.dto.ts b/src/whatsapp/dto/sqs.dto.ts new file mode 100644 index 00000000..9b8aeedd --- /dev/null +++ b/src/whatsapp/dto/sqs.dto.ts @@ -0,0 +1,4 @@ +export class SqsDto { + enabled: boolean; + events?: string[]; +} diff --git a/src/whatsapp/models/index.ts b/src/whatsapp/models/index.ts index e79093f9..7903e5b5 100644 --- a/src/whatsapp/models/index.ts +++ b/src/whatsapp/models/index.ts @@ -7,6 +7,7 @@ export * from './message.model'; export * from './proxy.model'; export * from './rabbitmq.model'; export * from './settings.model'; +export * from './sqs.model'; export * from './typebot.model'; export * from './webhook.model'; export * from './websocket.model'; diff --git a/src/whatsapp/models/message.model.ts b/src/whatsapp/models/message.model.ts index 252cd6e4..395b100b 100644 --- a/src/whatsapp/models/message.model.ts +++ b/src/whatsapp/models/message.model.ts @@ -22,6 +22,7 @@ export class MessageRaw { source?: 'android' | 'web' | 'ios'; source_id?: string; source_reply_id?: string; + chatwootMessageId?: string; } const messageSchema = new Schema({ @@ -39,8 +40,14 @@ const messageSchema = new Schema({ source: { type: String, minlength: 3, enum: ['android', 'web', 'ios'] }, messageTimestamp: { type: Number, required: true }, owner: { type: String, required: true, minlength: 1 }, + chatwootMessageId: { type: String, required: false }, }); +messageSchema.index({ chatwootMessageId: 1, owner: 1 }); +messageSchema.index({ 'key.id': 1 }); +messageSchema.index({ 'key.id': 1, owner: 1 }); +messageSchema.index({ owner: 1 }); + export const MessageModel = dbserver?.model(MessageRaw.name, messageSchema, 'messages'); export type IMessageModel = typeof MessageModel; diff --git a/src/whatsapp/models/sqs.model.ts b/src/whatsapp/models/sqs.model.ts new file mode 100644 index 00000000..2d5f432f --- /dev/null +++ b/src/whatsapp/models/sqs.model.ts @@ -0,0 +1,18 @@ +import { Schema } from 'mongoose'; + +import { dbserver } from '../../libs/db.connect'; + +export class SqsRaw { + _id?: string; + enabled?: boolean; + events?: string[]; +} + +const sqsSchema = new Schema({ + _id: { type: String, _id: true }, + enabled: { type: Boolean, required: true }, + events: { type: [String], required: true }, +}); + +export const SqsModel = dbserver?.model(SqsRaw.name, sqsSchema, 'sqs'); +export type ISqsModel = typeof SqsModel; diff --git a/src/whatsapp/repository/message.repository.ts b/src/whatsapp/repository/message.repository.ts index ed362815..e212ca3d 100644 --- a/src/whatsapp/repository/message.repository.ts +++ b/src/whatsapp/repository/message.repository.ts @@ -144,4 +144,55 @@ export class MessageRepository extends Repository { return []; } } + + public async update(data: MessageRaw[], instanceName: string, saveDb?: boolean): Promise { + try { + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('updating messages in db'); + + const messages = data.map((message) => { + return { + updateOne: { + filter: { 'key.id': message.key.id }, + update: { ...message }, + }, + }; + }); + + const { nModified } = await this.messageModel.bulkWrite(messages); + + this.logger.verbose('messages updated in db: ' + nModified + ' messages'); + return { insertCount: nModified }; + } + + this.logger.verbose('updating messages in store'); + + const store = this.configService.get('STORE'); + + if (store.MESSAGES) { + this.logger.verbose('updating messages in store'); + data.forEach((message) => { + this.writeStore({ + path: join(this.storePath, 'messages', instanceName), + fileName: message.key.id, + data: message, + }); + this.logger.verbose( + 'messages updated in store in path: ' + + join(this.storePath, 'messages', instanceName) + + '/' + + message.key.id, + ); + }); + + this.logger.verbose('messages updated in store: ' + data.length + ' messages'); + return { insertCount: data.length }; + } + + this.logger.verbose('messages not updated'); + return { insertCount: 0 }; + } catch (error) { + this.logger.error(error); + } + } } diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index 1c16fdef..ab4da1e3 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -14,6 +14,7 @@ import { MessageUpRepository } from './messageUp.repository'; import { ProxyRepository } from './proxy.repository'; import { RabbitmqRepository } from './rabbitmq.repository'; import { SettingsRepository } from './settings.repository'; +import { SqsRepository } from './sqs.repository'; import { TypebotRepository } from './typebot.repository'; import { WebhookRepository } from './webhook.repository'; import { WebsocketRepository } from './websocket.repository'; @@ -28,6 +29,7 @@ export class RepositoryBroker { public readonly settings: SettingsRepository, public readonly websocket: WebsocketRepository, public readonly rabbitmq: RabbitmqRepository, + public readonly sqs: SqsRepository, public readonly typebot: TypebotRepository, public readonly proxy: ProxyRepository, public readonly chamaai: ChamaaiRepository, @@ -63,6 +65,7 @@ export class RepositoryBroker { const settingsDir = join(storePath, 'settings'); const websocketDir = join(storePath, 'websocket'); const rabbitmqDir = join(storePath, 'rabbitmq'); + const sqsDir = join(storePath, 'sqs'); const typebotDir = join(storePath, 'typebot'); const proxyDir = join(storePath, 'proxy'); const chamaaiDir = join(storePath, 'chamaai'); @@ -108,6 +111,10 @@ export class RepositoryBroker { this.logger.verbose('creating rabbitmq dir: ' + rabbitmqDir); fs.mkdirSync(rabbitmqDir, { recursive: true }); } + if (!fs.existsSync(sqsDir)) { + this.logger.verbose('creating sqs dir: ' + sqsDir); + fs.mkdirSync(sqsDir, { recursive: true }); + } if (!fs.existsSync(typebotDir)) { this.logger.verbose('creating typebot dir: ' + typebotDir); fs.mkdirSync(typebotDir, { recursive: true }); diff --git a/src/whatsapp/repository/sqs.repository.ts b/src/whatsapp/repository/sqs.repository.ts new file mode 100644 index 00000000..50ea1cd3 --- /dev/null +++ b/src/whatsapp/repository/sqs.repository.ts @@ -0,0 +1,62 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; + +import { ConfigService } from '../../config/env.config'; +import { Logger } from '../../config/logger.config'; +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { ISqsModel, SqsRaw } from '../models'; + +export class SqsRepository extends Repository { + constructor(private readonly sqsModel: ISqsModel, private readonly configService: ConfigService) { + super(configService); + } + + private readonly logger = new Logger('SqsRepository'); + + public async create(data: SqsRaw, instance: string): Promise { + try { + this.logger.verbose('creating sqs'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('saving sqs to db'); + const insert = await this.sqsModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); + + this.logger.verbose('sqs saved to db: ' + insert.modifiedCount + ' sqs'); + return { insertCount: insert.modifiedCount }; + } + + this.logger.verbose('saving sqs to store'); + + this.writeStore({ + path: join(this.storePath, 'sqs'), + fileName: instance, + data, + }); + + this.logger.verbose('sqs saved to store in path: ' + join(this.storePath, 'sqs') + '/' + instance); + + this.logger.verbose('sqs created'); + return { insertCount: 1 }; + } catch (error) { + return error; + } + } + + public async find(instance: string): Promise { + try { + this.logger.verbose('finding sqs'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding sqs in db'); + return await this.sqsModel.findOne({ _id: instance }); + } + + this.logger.verbose('finding sqs in store'); + return JSON.parse( + readFileSync(join(this.storePath, 'sqs', instance + '.json'), { + encoding: 'utf-8', + }), + ) as SqsRaw; + } catch (error) { + return {}; + } + } +} diff --git a/src/whatsapp/routers/chat.router.ts b/src/whatsapp/routers/chat.router.ts index 285c29a0..29d1cdc3 100644 --- a/src/whatsapp/routers/chat.router.ts +++ b/src/whatsapp/routers/chat.router.ts @@ -7,6 +7,7 @@ import { deleteMessageSchema, messageUpSchema, messageValidateSchema, + presenceSchema, privacySettingsSchema, profileNameSchema, profilePictureSchema, @@ -26,6 +27,7 @@ import { ProfilePictureDto, ProfileStatusDto, ReadMessageDto, + SendPresenceDto, WhatsAppNumberDto, } from '../dto/chat.dto'; import { InstanceDto } from '../dto/instance.dto'; @@ -228,6 +230,22 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) + .post(this.routerPath('sendPresence'), ...guards, async (req, res) => { + logger.verbose('request received in sendPresence'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: presenceSchema, + ClassRef: SendPresenceDto, + execute: (instance, data) => chatController.sendPresence(instance, data), + }); + + return res.status(HttpStatus.CREATED).json(response); + }) // Profile routes .get(this.routerPath('fetchPrivacySettings'), ...guards, async (req, res) => { logger.verbose('request received in fetchPrivacySettings'); diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index e35d21e4..3dd4492b 100644 --- a/src/whatsapp/routers/index.router.ts +++ b/src/whatsapp/routers/index.router.ts @@ -13,6 +13,7 @@ import { ProxyRouter } from './proxy.router'; import { RabbitmqRouter } from './rabbitmq.router'; import { MessageRouter } from './sendMessage.router'; import { SettingsRouter } from './settings.router'; +import { SqsRouter } from './sqs.router'; import { TypebotRouter } from './typebot.router'; import { ViewsRouter } from './view.router'; import { WebhookRouter } from './webhook.router'; @@ -41,6 +42,7 @@ router message: 'Welcome to the Evolution API, it is working!', version: packageJson.version, documentation: `${req.protocol}://${req.get('host')}/docs`, + manager: `${req.protocol}://${req.get('host')}/manager`, }); }) .use('/instance', new InstanceRouter(configService, ...guards).router) @@ -53,6 +55,7 @@ router .use('/settings', new SettingsRouter(...guards).router) .use('/websocket', new WebsocketRouter(...guards).router) .use('/rabbitmq', new RabbitmqRouter(...guards).router) + .use('/sqs', new SqsRouter(...guards).router) .use('/typebot', new TypebotRouter(...guards).router) .use('/proxy', new ProxyRouter(...guards).router) .use('/chamaai', new ChamaaiRouter(...guards).router); diff --git a/src/whatsapp/routers/sqs.router.ts b/src/whatsapp/routers/sqs.router.ts new file mode 100644 index 00000000..e1bf8e93 --- /dev/null +++ b/src/whatsapp/routers/sqs.router.ts @@ -0,0 +1,52 @@ +import { RequestHandler, Router } from 'express'; + +import { Logger } from '../../config/logger.config'; +import { instanceNameSchema, sqsSchema } from '../../validate/validate.schema'; +import { RouterBroker } from '../abstract/abstract.router'; +import { InstanceDto } from '../dto/instance.dto'; +import { SqsDto } from '../dto/sqs.dto'; +import { sqsController } from '../whatsapp.module'; +import { HttpStatus } from './index.router'; + +const logger = new Logger('SqsRouter'); + +export class SqsRouter extends RouterBroker { + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('set'), ...guards, async (req, res) => { + logger.verbose('request received in setSqs'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: sqsSchema, + ClassRef: SqsDto, + execute: (instance, data) => sqsController.createSqs(instance, data), + }); + + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('find'), ...guards, async (req, res) => { + logger.verbose('request received in findSqs'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance) => sqsController.findSqs(instance), + }); + + res.status(HttpStatus.OK).json(response); + }); + } + + public readonly router = Router(); +} diff --git a/src/whatsapp/routers/view.router.ts b/src/whatsapp/routers/view.router.ts index 11002777..ecfe64c1 100644 --- a/src/whatsapp/routers/view.router.ts +++ b/src/whatsapp/routers/view.router.ts @@ -1,14 +1,32 @@ import { Router } from 'express'; +import fs from 'fs'; +import mime from 'mime-types'; import { RouterBroker } from '../abstract/abstract.router'; -import { viewsController } from '../whatsapp.module'; export class ViewsRouter extends RouterBroker { constructor() { super(); - this.router.get('/', (req, res) => { - return viewsController.manager(req, res); + const basePath = 'evolution-manager/dist'; + + const indexPath = require.resolve(`${basePath}/index.html`); + + this.router.get('/*', (req, res) => { + try { + const pathname = req.url.split('?')[0]; + + // verify if url is a file in dist folder + if (pathname === '/') throw {}; + const filePath = require.resolve(`${basePath}${pathname}`); + + const contentType = mime.lookup(filePath) || 'text/plain'; + res.set('Content-Type', contentType); + res.end(fs.readFileSync(filePath)); + } catch { + res.set('Content-Type', 'text/html'); + res.send(fs.readFileSync(indexPath)); + } }); } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index b19fa31f..a995f367 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -6,12 +6,14 @@ import Jimp from 'jimp'; import mimeTypes from 'mime-types'; import path from 'path'; -import { ConfigService } from '../../config/env.config'; +import { ConfigService, HttpServer } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { ROOT_DIR } from '../../config/path.config'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; -import { SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; +import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; +import { MessageRaw } from '../models'; +import { RepositoryBroker } from '../repository/repository.manager'; import { WAMonitoringService } from './monitor.service'; export class ChatwootService { @@ -22,7 +24,11 @@ export class ChatwootService { private provider: any; - constructor(private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService) { + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly configService: ConfigService, + private readonly repository: RepositoryBroker, + ) { this.messageCache = new Set(); } @@ -52,25 +58,26 @@ export class ChatwootService { private async getProvider(instance: InstanceDto) { this.logger.verbose('get provider to instance: ' + instance.instanceName); - try { - const provider = await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); + const provider = await this.waMonitor.waInstances[instance.instanceName]?.findChatwoot(); - if (!provider) { - this.logger.warn('provider not found'); - return null; - } - - this.logger.verbose('provider found'); - - return provider; - } catch (error) { - this.logger.error('provider not found'); + if (!provider) { + this.logger.warn('provider not found'); return null; } + + this.logger.verbose('provider found'); + + return provider; + // try { + // } catch (error) { + // this.logger.error('provider not found'); + // return null; + // } } private async clientCw(instance: InstanceDto) { this.logger.verbose('get client to instance: ' + instance.instanceName); + const provider = await this.getProvider(instance); if (!provider) { @@ -97,11 +104,24 @@ export class ChatwootService { return client; } - public create(instance: InstanceDto, data: ChatwootDto) { + public async create(instance: InstanceDto, data: ChatwootDto) { this.logger.verbose('create chatwoot: ' + instance.instanceName); - this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); + + await this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); this.logger.verbose('chatwoot created'); + + if (data.auto_create) { + const urlServer = this.configService.get('SERVER').URL; + + await this.initInstanceChatwoot( + instance, + instance.instanceName.split('-cwId-')[0], + `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, + true, + data.number, + ); + } return data; } @@ -229,10 +249,6 @@ export class ChatwootService { inbox_id: inboxId.toString(), }; - if (this.provider.conversation_pending) { - data['status'] = 'pending'; - } - const conversation = await client.conversations.create({ accountId: this.provider.account_id, data, @@ -338,14 +354,18 @@ export class ChatwootService { } this.logger.verbose('update contact in chatwoot'); - const contact = await client.contacts.update({ - accountId: this.provider.account_id, - id, - data, - }); + try { + const contact = await client.contacts.update({ + accountId: this.provider.account_id, + id, + data, + }); - this.logger.verbose('contact updated'); - return contact; + this.logger.verbose('contact updated'); + return contact; + } catch (error) { + this.logger.error(error); + } } public async findContact(instance: InstanceDto, phoneNumber: string) { @@ -488,6 +508,9 @@ export class ChatwootService { avatar_url: picture_url.profilePictureUrl || null, }); } + if (!contact) { + contact = await this.findContact(instance, chatId); + } } else { const jid = isGroup ? null : body.key.remoteJid; contact = await this.createContact( @@ -619,6 +642,7 @@ export class ChatwootService { encoding: string; filename: string; }[], + messageBody?: any, ) { this.logger.verbose('create message to instance: ' + instance.instanceName); @@ -629,6 +653,8 @@ export class ChatwootService { return null; } + const replyToIds = await this.getReplyToIds(messageBody, instance); + this.logger.verbose('create message in chatwoot'); const message = await client.messages.create({ accountId: this.provider.account_id, @@ -638,6 +664,9 @@ export class ChatwootService { message_type: messageType, attachments: attachments, private: privateMessage || false, + content_attributes: { + ...replyToIds, + }, }, }); @@ -733,6 +762,8 @@ export class ChatwootService { file: string, messageType: 'incoming' | 'outgoing' | undefined, content?: string, + instance?: InstanceDto, + messageBody?: any, ) { this.logger.verbose('send data to chatwoot'); @@ -749,6 +780,16 @@ export class ChatwootService { this.logger.verbose('temp file found'); data.append('attachments[]', createReadStream(file)); + if (messageBody && instance) { + const replyToIds = await this.getReplyToIds(messageBody, instance); + + if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) { + data.append('content_attributes', { + ...replyToIds, + }); + } + } + this.logger.verbose('get client to instance: ' + this.provider.instanceName); const config = { method: 'post', @@ -869,7 +910,7 @@ export class ChatwootService { } } - public async sendAttachment(waInstance: any, number: string, media: any, caption?: string) { + public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) { this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); try { @@ -911,13 +952,14 @@ export class ChatwootService { options: { delay: 1200, presence: 'recording', + ...options, }, }; - await waInstance?.audioWhatsapp(data, true); + const messageSent = await waInstance?.audioWhatsapp(data, true); this.logger.verbose('audio sent'); - return; + return messageSent; } this.logger.verbose('send media to instance: ' + waInstance.instanceName); @@ -931,6 +973,7 @@ export class ChatwootService { options: { delay: 1200, presence: 'composing', + ...options, }, }; @@ -939,10 +982,10 @@ export class ChatwootService { data.mediaMessage.caption = caption; } - await waInstance?.mediaMessage(data, true); + const messageSent = await waInstance?.mediaMessage(data, true); this.logger.verbose('media sent'); - return; + return messageSent; } catch (error) { this.logger.error(error); } @@ -961,7 +1004,13 @@ export class ChatwootService { } this.logger.verbose('check if is bot'); - if (!body?.conversation || body.private || body.event === 'message_updated') return { message: 'bot' }; + if ( + !body?.conversation || + body.private || + (body.event === 'message_updated' && !body.content_attributes?.deleted) + ) { + return { message: 'bot' }; + } this.logger.verbose('check if is group'); const chatId = @@ -970,6 +1019,21 @@ export class ChatwootService { const senderName = body?.sender?.name; const waInstance = this.waMonitor.waInstances[instance.instanceName]; + this.logger.verbose('check if is a message deletion'); + if (body.event === 'message_updated' && body.content_attributes?.deleted) { + const message = await this.repository.message.find({ + where: { + owner: instance.instanceName, + chatwootMessageId: body.id, + }, + limit: 1, + }); + if (message.length && message[0].key?.id) { + await waInstance?.client.sendMessage(message[0].key.remoteJid, { delete: message[0].key }); + } + return { message: 'bot' }; + } + if (chatId === '123456' && body.message_type === 'outgoing') { this.logger.verbose('check if is command'); @@ -980,6 +1044,10 @@ export class ChatwootService { const state = waInstance?.connectionStatus?.state; if (state !== 'open') { + if (state === 'close') { + this.logger.verbose('request cleaning up instance: ' + instance.instanceName); + // await this.waMonitor.cleaningUp(instance.instanceName); + } this.logger.verbose('connect to whatsapp'); const number = command.split(':')[1]; await waInstance.connectToWhatsapp(number); @@ -1057,7 +1125,26 @@ export class ChatwootService { formatText = null; } - await this.sendAttachment(waInstance, chatId, attachment.data_url, formatText); + const options: Options = { + quoted: await this.getQuotedMessage(body, instance), + }; + + const messageSent = await this.sendAttachment( + waInstance, + chatId, + attachment.data_url, + formatText, + options, + ); + + this.updateChatwootMessageId( + { + ...messageSent, + owner: instance.instanceName, + }, + body.id, + instance, + ); } } else { this.logger.verbose('message is text'); @@ -1071,10 +1158,20 @@ export class ChatwootService { options: { delay: 1200, presence: 'composing', + quoted: await this.getQuotedMessage(body, instance), }, }; - await waInstance?.textMessage(data, true); + const messageSent = await waInstance?.textMessage(data, true); + + this.updateChatwootMessageId( + { + ...messageSent, + owner: instance.instanceName, + }, + body.id, + instance, + ); } } } @@ -1106,6 +1203,65 @@ export class ChatwootService { } } + private updateChatwootMessageId(message: MessageRaw, chatwootMessageId: string, instance: InstanceDto) { + if (!chatwootMessageId) { + return; + } + message.chatwootMessageId = chatwootMessageId; + this.repository.message.update([message], instance.instanceName, true); + } + + private async getReplyToIds( + msg: any, + instance: InstanceDto, + ): Promise<{ in_reply_to: string; in_reply_to_external_id: string }> { + let inReplyTo = null; + let inReplyToExternalId = null; + + if (msg) { + inReplyToExternalId = msg.message?.extendedTextMessage?.contextInfo?.stanzaId; + if (inReplyToExternalId) { + const message = await this.repository.message.find({ + where: { + key: { + id: inReplyToExternalId, + }, + owner: instance.instanceName, + }, + limit: 1, + }); + if (message.length && message[0]?.chatwootMessageId) { + inReplyTo = message[0].chatwootMessageId; + } + } + } + + return { + in_reply_to: inReplyTo, + in_reply_to_external_id: inReplyToExternalId, + }; + } + + private async getQuotedMessage(msg: any, instance: InstanceDto): Promise { + if (msg?.content_attributes?.in_reply_to) { + const message = await this.repository.message.find({ + where: { + chatwootMessageId: msg?.content_attributes?.in_reply_to, + owner: instance.instanceName, + }, + limit: 1, + }); + if (message.length && message[0]?.key?.id) { + return { + key: message[0].key, + message: message[0].message, + }; + } + } + + return null; + } + private isMediaMessage(message: any) { this.logger.verbose('check if is media message'); const media = [ @@ -1139,6 +1295,18 @@ export class ChatwootService { return adsMessage; } + private getReactionMessage(msg: any) { + interface ReactionMessage { + key: MessageRaw['key']; + text: string; + } + const reactionMessage: ReactionMessage | undefined = msg?.reactionMessage; + + this.logger.verbose('Get reaction message if it exists'); + reactionMessage && this.logger.verbose('Reaction message: ' + reactionMessage); + return reactionMessage; + } + private getTypeMessage(msg: any) { this.logger.verbose('get type message'); @@ -1205,6 +1373,11 @@ export class ChatwootService { formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; numberCount++; } + if (key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += `\n**number:** ${phoneNumber}`; + numberCount++; + } }); this.logger.verbose('message content: ' + formattedContact); @@ -1233,6 +1406,11 @@ export class ChatwootService { formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; numberCount++; } + if (key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += `\n**number:** ${phoneNumber}`; + numberCount++; + } }); return formattedContact; @@ -1265,13 +1443,6 @@ export class ChatwootService { public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); try { - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - const waInstance = this.waMonitor.waInstances[instance.instanceName]; if (!waInstance) { @@ -1279,6 +1450,13 @@ export class ChatwootService { return null; } + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + if (event === 'messages.upsert' || event === 'send.message') { this.logger.verbose('event messages.upsert'); @@ -1290,11 +1468,18 @@ export class ChatwootService { this.logger.verbose('get conversation message'); const bodyMessage = await this.getConversationMessage(body.message); + if (bodyMessage && bodyMessage.includes('Por favor, classifique esta conversa, http')) { + this.logger.verbose('conversation is closed'); + return; + } + const isMedia = this.isMediaMessage(body.message); const adsMessage = this.getAdsMessage(body.message); - if (!bodyMessage && !isMedia) { + const reactionMessage = this.getReactionMessage(body.message); + + if (!bodyMessage && !isMedia && !reactionMessage) { this.logger.warn('no body message found'); return; } @@ -1353,7 +1538,7 @@ export class ChatwootService { } this.logger.verbose('send data to chatwoot'); - const send = await this.sendData(getConversation, fileName, messageType, content); + const send = await this.sendData(getConversation, fileName, messageType, content, instance, body); if (!send) { this.logger.warn('message not sent'); @@ -1374,7 +1559,7 @@ export class ChatwootService { this.logger.verbose('message is not group'); this.logger.verbose('send data to chatwoot'); - const send = await this.sendData(getConversation, fileName, messageType, bodyMessage); + const send = await this.sendData(getConversation, fileName, messageType, bodyMessage, instance, body); if (!send) { this.logger.warn('message not sent'); @@ -1394,6 +1579,35 @@ export class ChatwootService { } } + this.logger.verbose('check if has ReactionMessage'); + if (reactionMessage) { + this.logger.verbose('send data to chatwoot'); + if (reactionMessage.text) { + const send = await this.createMessage( + instance, + getConversation, + reactionMessage.text, + messageType, + false, + [], + { + message: { extendedTextMessage: { contextInfo: { stanzaId: reactionMessage.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; + } + this.logger.verbose('check if has Ads Message'); if (adsMessage) { this.logger.verbose('message is from Ads'); @@ -1436,6 +1650,8 @@ export class ChatwootService { fileName, messageType, `${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`, + instance, + body, ); if (!send) { @@ -1471,7 +1687,7 @@ export class ChatwootService { } this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage(instance, getConversation, content, messageType); + const send = await this.createMessage(instance, getConversation, content, messageType, false, [], body); if (!send) { this.logger.warn('message not sent'); @@ -1492,7 +1708,7 @@ export class ChatwootService { this.logger.verbose('message is not group'); this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage(instance, getConversation, bodyMessage, messageType); + const send = await this.createMessage(instance, getConversation, bodyMessage, messageType, false, [], body); if (!send) { this.logger.warn('message not sent'); @@ -1528,16 +1744,18 @@ export class ChatwootService { await this.createBotMessage(instance, msgStatus, 'incoming'); } - // if (event === 'connection.update') { - // this.logger.verbose('event connection.update'); + if (event === 'connection.update') { + this.logger.verbose('event connection.update'); - // if (body.status === 'open') { - // const msgConnection = `🚀 Connection successfully established!`; - - // this.logger.verbose('send message to chatwoot'); - // await this.createBotMessage(instance, msgConnection, 'incoming'); - // } - // } + if (body.status === 'open') { + // if we have qrcode count then we understand that a new connection was established + if (this.waMonitor.waInstances[instance.instanceName].qrCode.count > 0) { + const msgConnection = `🚀 Connection successfully established!`; + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgConnection, 'incoming'); + } + } + } if (event === 'qrcode.updated') { this.logger.verbose('event qrcode.updated'); diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 1decd13c..766569de 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -12,12 +12,18 @@ import { dbserver } from '../../libs/db.connect'; import { RedisCache } from '../../libs/redis.client'; import { AuthModel, + ChamaaiModel, + // ChatModel, ChatwootModel, - ContactModel, - MessageModel, - MessageUpModel, + // ContactModel, + // MessageModel, + // MessageUpModel, + ProxyModel, + RabbitmqModel, SettingsModel, + TypebotModel, WebhookModel, + WebsocketModel, } from '../models'; import { RepositoryBroker } from '../repository/repository.manager'; import { WAStartupService } from './whatsapp.service'; @@ -33,7 +39,7 @@ export class WAMonitoringService { this.removeInstance(); this.noConnection(); - this.delInstanceFiles(); + // this.delInstanceFiles(); Object.assign(this.db, configService.get('DATABASE')); Object.assign(this.redis, configService.get('REDIS')); @@ -117,7 +123,7 @@ export class WAMonitoringService { if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { instanceData.instance['serverUrl'] = this.configService.get('SERVER').URL; - instanceData.instance['apikey'] = (await this.repository.auth.find(key)).apikey; + instanceData.instance['apikey'] = (await this.repository.auth.find(key))?.apikey; instanceData.instance['chatwoot'] = chatwoot; } @@ -136,7 +142,7 @@ export class WAMonitoringService { if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { instanceData.instance['serverUrl'] = this.configService.get('SERVER').URL; - instanceData.instance['apikey'] = (await this.repository.auth.find(key)).apikey; + instanceData.instance['apikey'] = (await this.repository.auth.find(key))?.apikey; instanceData.instance['chatwoot'] = chatwoot; } @@ -187,6 +193,13 @@ export class WAMonitoringService { public async cleaningUp(instanceName: string) { this.logger.verbose('cleaning up instance: ' + instanceName); + if (this.redis.ENABLED) { + this.logger.verbose('cleaning up instance in redis: ' + instanceName); + this.cache.reference = instanceName; + await this.cache.delAll(); + return; + } + if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { this.logger.verbose('cleaning up instance in database: ' + instanceName); await this.repository.dbServer.connect(); @@ -197,13 +210,6 @@ export class WAMonitoringService { return; } - if (this.redis.ENABLED) { - this.logger.verbose('cleaning up instance in redis: ' + instanceName); - this.cache.reference = instanceName; - await this.cache.delAll(); - return; - } - this.logger.verbose('cleaning up instance in files: ' + instanceName); rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); } @@ -233,13 +239,19 @@ export class WAMonitoringService { this.logger.verbose('cleaning store database instance: ' + instanceName); - await AuthModel.deleteMany({ owner: instanceName }); - await ContactModel.deleteMany({ owner: instanceName }); - await MessageModel.deleteMany({ owner: instanceName }); - await MessageUpModel.deleteMany({ owner: instanceName }); + // await ChatModel.deleteMany({ owner: instanceName }); + // await ContactModel.deleteMany({ owner: instanceName }); + // await MessageUpModel.deleteMany({ owner: instanceName }); + // await MessageModel.deleteMany({ owner: instanceName }); + await AuthModel.deleteMany({ _id: instanceName }); await WebhookModel.deleteMany({ _id: instanceName }); await ChatwootModel.deleteMany({ _id: instanceName }); + await ChamaaiModel.deleteMany({ _id: instanceName }); + await ProxyModel.deleteMany({ _id: instanceName }); + await RabbitmqModel.deleteMany({ _id: instanceName }); + await TypebotModel.deleteMany({ _id: instanceName }); + await WebsocketModel.deleteMany({ _id: instanceName }); await SettingsModel.deleteMany({ _id: instanceName }); return; @@ -265,7 +277,6 @@ export class WAMonitoringService { const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); instance.instanceName = name; this.logger.verbose('Instance loaded: ' + name); - await instance.connectToWhatsapp(); this.logger.verbose('connectToWhatsapp: ' + name); @@ -346,8 +357,8 @@ export class WAMonitoringService { this.eventEmitter.on('logout.instance', async (instanceName: string) => { this.logger.verbose('logout instance: ' + instanceName); try { - this.logger.verbose('request cleaning up instance: ' + instanceName); - this.cleaningUp(instanceName); + // this.logger.verbose('request cleaning up instance: ' + instanceName); + // this.cleaningUp(instanceName); } finally { this.logger.warn(`Instance "${instanceName}" - LOGOUT`); } diff --git a/src/whatsapp/services/proxy.service.ts b/src/whatsapp/services/proxy.service.ts index c6631671..1039fd5c 100644 --- a/src/whatsapp/services/proxy.service.ts +++ b/src/whatsapp/services/proxy.service.ts @@ -9,9 +9,9 @@ export class ProxyService { private readonly logger = new Logger(ProxyService.name); - public create(instance: InstanceDto, data: ProxyDto) { + public create(instance: InstanceDto, data: ProxyDto, reload = true) { this.logger.verbose('create proxy: ' + instance.instanceName); - this.waMonitor.waInstances[instance.instanceName].setProxy(data); + this.waMonitor.waInstances[instance.instanceName].setProxy(data, reload); return { proxy: { ...instance, proxy: data } }; } diff --git a/src/whatsapp/services/settings.service.ts b/src/whatsapp/services/settings.service.ts index 6815ca40..741a2cbc 100644 --- a/src/whatsapp/services/settings.service.ts +++ b/src/whatsapp/services/settings.service.ts @@ -26,7 +26,7 @@ export class SettingsService { return result; } catch (error) { - return { reject_call: false, msg_call: '', groups_ignore: false }; + return { reject_call: false, msg_call: '', groups_ignore: true }; } } } diff --git a/src/whatsapp/services/sqs.service.ts b/src/whatsapp/services/sqs.service.ts new file mode 100644 index 00000000..236d4ceb --- /dev/null +++ b/src/whatsapp/services/sqs.service.ts @@ -0,0 +1,35 @@ +import { Logger } from '../../config/logger.config'; +import { initQueues } from '../../libs/sqs.server'; +import { InstanceDto } from '../dto/instance.dto'; +import { SqsDto } from '../dto/sqs.dto'; +import { SqsRaw } from '../models'; +import { WAMonitoringService } from './monitor.service'; + +export class SqsService { + constructor(private readonly waMonitor: WAMonitoringService) {} + + private readonly logger = new Logger(SqsService.name); + + public create(instance: InstanceDto, data: SqsDto) { + this.logger.verbose('create sqs: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setSqs(data); + + initQueues(instance.instanceName, data.events); + return { sqs: { ...instance, sqs: data } }; + } + + public async find(instance: InstanceDto): Promise { + try { + this.logger.verbose('find sqs: ' + instance.instanceName); + const result = await this.waMonitor.waInstances[instance.instanceName].findSqs(); + + if (Object.keys(result).length === 0) { + throw new Error('Sqs not found'); + } + + return result; + } catch (error) { + return { enabled: false, events: [] }; + } + } +} diff --git a/src/whatsapp/services/typebot.service.ts b/src/whatsapp/services/typebot.service.ts index c329a169..0da51193 100644 --- a/src/whatsapp/services/typebot.service.ts +++ b/src/whatsapp/services/typebot.service.ts @@ -1,5 +1,6 @@ import axios from 'axios'; +import { ConfigService, Typebot } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { InstanceDto } from '../dto/instance.dto'; import { Session, TypebotDto } from '../dto/typebot.dto'; @@ -8,7 +9,7 @@ import { Events } from '../types/wa.types'; import { WAMonitoringService } from './monitor.service'; export class TypebotService { - constructor(private readonly waMonitor: WAMonitoringService) {} + constructor(private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService) {} private readonly logger = new Logger(TypebotService.name); @@ -47,7 +48,7 @@ export class TypebotService { findData.sessions.splice(findData.sessions.indexOf(session), 1); const typebotData = { - enabled: true, + enabled: findData.enabled, url: findData.url, typebot: findData.typebot, expire: findData.expire, @@ -68,10 +69,24 @@ export class TypebotService { session.status = status; } }); + } else if (status === 'paused') { + const session: Session = { + remoteJid: remoteJid, + sessionId: Math.floor(Math.random() * 10000000000).toString(), + status: status, + createdAt: Date.now(), + updateAt: Date.now(), + prefilledVariables: { + remoteJid: remoteJid, + pushName: '', + additionalData: {}, + }, + }; + findData.sessions.push(session); } const typebotData = { - enabled: true, + enabled: findData.enabled, url: findData.url, typebot: findData.typebot, expire: findData.expire, @@ -96,13 +111,14 @@ export class TypebotService { } public async startTypebot(instance: InstanceDto, data: any) { + if (data.remoteJid === 'status@broadcast') return; + const remoteJid = data.remoteJid; const url = data.url; const typebot = data.typebot; const startSession = data.startSession; const variables = data.variables; const findTypebot = await this.find(instance); - const sessions = (findTypebot.sessions as Session[]) ?? []; const expire = findTypebot.expire; const keyword_finish = findTypebot.keyword_finish; const delay_message = findTypebot.delay_message; @@ -121,7 +137,10 @@ export class TypebotService { } if (startSession) { + const newSessions = await this.clearSessions(instance, remoteJid); + const response = await this.createNewSession(instance, { + enabled: findTypebot.enabled, url: url, typebot: typebot, remoteJid: remoteJid, @@ -130,7 +149,7 @@ export class TypebotService { delay_message: delay_message, unknown_message: unknown_message, listening_from_me: listening_from_me, - sessions: sessions, + sessions: newSessions, prefilledVariables: prefilledVariables, }); @@ -152,28 +171,40 @@ export class TypebotService { const reqData = { startParams: { - typebot: data.typebot, + publicId: data.typebot, prefilledVariables: prefilledVariables, }, }; - const request = await axios.post(data.url + '/api/v1/sendMessage', reqData); + try { + const version = this.configService.get('TYPEBOT').API_VERSION; + let url: string; + if (version === 'latest') { + url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`; + } else { + url = `${data.url}/api/v1/sendMessage`; + } + const request = await axios.post(url, reqData); - await this.sendWAMessage( - instance, - remoteJid, - request.data.messages, - request.data.input, - request.data.clientSideActions, - ); + await this.sendWAMessage( + instance, + remoteJid, + request.data.messages, + request.data.input, + request.data.clientSideActions, + ); - this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, { - remoteJid: remoteJid, - url: url, - typebot: typebot, - variables: variables, - sessionId: id, - }); + this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, { + remoteJid: remoteJid, + url: url, + typebot: typebot, + variables: variables, + sessionId: id, + }); + } catch (error) { + this.logger.error(error); + return; + } } return { @@ -226,52 +257,96 @@ export class TypebotService { } public async createNewSession(instance: InstanceDto, data: any) { + if (data.remoteJid === 'status@broadcast') return; const id = Math.floor(Math.random() * 10000000000).toString(); + const reqData = { startParams: { - typebot: data.typebot, + publicId: data.typebot, prefilledVariables: { ...data.prefilledVariables, remoteJid: data.remoteJid, - pushName: data.pushName || '', + pushName: data.pushName || data.prefilledVariables?.pushName || '', instanceName: instance.instanceName, }, }, }; - const request = await axios.post(data.url + '/api/v1/sendMessage', reqData); + try { + const version = this.configService.get('TYPEBOT').API_VERSION; + let url: string; + if (version === 'latest') { + url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`; + } else { + url = `${data.url}/api/v1/sendMessage`; + } + const request = await axios.post(url, reqData); - if (request.data.sessionId) { - data.sessions.push({ - remoteJid: data.remoteJid, - sessionId: `${id}-${request.data.sessionId}`, - status: 'opened', - createdAt: Date.now(), - updateAt: Date.now(), - prefilledVariables: { - ...data.prefilledVariables, + if (request?.data?.sessionId) { + data.sessions.push({ remoteJid: data.remoteJid, - pushName: data.pushName || '', - instanceName: instance.instanceName, - }, + sessionId: `${id}-${request.data.sessionId}`, + status: 'opened', + createdAt: Date.now(), + updateAt: Date.now(), + prefilledVariables: { + ...data.prefilledVariables, + remoteJid: data.remoteJid, + pushName: data.pushName || '', + instanceName: instance.instanceName, + }, + }); + + const typebotData = { + enabled: data.enabled, + url: data.url, + typebot: data.typebot, + expire: data.expire, + keyword_finish: data.keyword_finish, + delay_message: data.delay_message, + unknown_message: data.unknown_message, + listening_from_me: data.listening_from_me, + sessions: data.sessions, + }; + + this.create(instance, typebotData); + } + return request.data; + } catch (error) { + this.logger.error(error); + return; + } + } + + public async clearSessions(instance: InstanceDto, remoteJid: string) { + const findTypebot = await this.find(instance); + const sessions = (findTypebot.sessions as Session[]) ?? []; + + const sessionWithRemoteJid = sessions.filter((session) => session.remoteJid === remoteJid); + + if (sessionWithRemoteJid.length > 0) { + sessionWithRemoteJid.forEach((session) => { + sessions.splice(sessions.indexOf(session), 1); }); const typebotData = { - enabled: true, - url: data.url, - typebot: data.typebot, - expire: data.expire, - keyword_finish: data.keyword_finish, - delay_message: data.delay_message, - unknown_message: data.unknown_message, - listening_from_me: data.listening_from_me, - sessions: data.sessions, + enabled: findTypebot.enabled, + url: findTypebot.url, + typebot: findTypebot.typebot, + expire: findTypebot.expire, + keyword_finish: findTypebot.keyword_finish, + delay_message: findTypebot.delay_message, + unknown_message: findTypebot.unknown_message, + listening_from_me: findTypebot.listening_from_me, + sessions, }; this.create(instance, typebotData); + + return sessions; } - return request.data; + return sessions; } public async sendWAMessage( @@ -323,7 +398,7 @@ export class TypebotService { } if (element.underline) { - text = `~${text}~`; + text = `*${text}*`; } if (element.url) { @@ -436,17 +511,115 @@ export class TypebotService { const session = sessions.find((session) => session.remoteJid === remoteJid); - if (session && expire && expire > 0) { - const now = Date.now(); + try { + if (session && expire && expire > 0) { + const now = Date.now(); - const diff = now - session.updateAt; + const diff = now - session.updateAt; - const diffInMinutes = Math.floor(diff / 1000 / 60); + const diffInMinutes = Math.floor(diff / 1000 / 60); - if (diffInMinutes > expire) { - sessions.splice(sessions.indexOf(session), 1); + if (diffInMinutes > expire) { + const newSessions = await this.clearSessions(instance, remoteJid); + const data = await this.createNewSession(instance, { + enabled: findTypebot.enabled, + url: url, + typebot: typebot, + expire: expire, + keyword_finish: keyword_finish, + delay_message: delay_message, + unknown_message: unknown_message, + listening_from_me: listening_from_me, + sessions: newSessions, + remoteJid: remoteJid, + pushName: msg.pushName, + }); + + await this.sendWAMessage(instance, remoteJid, data.messages, data.input, data.clientSideActions); + + if (data.messages.length === 0) { + const content = this.getConversationMessage(msg.message); + + if (!content) { + if (unknown_message) { + this.waMonitor.waInstances[instance.instanceName].textMessage({ + number: remoteJid.split('@')[0], + options: { + delay: delay_message || 1000, + presence: 'composing', + }, + textMessage: { + text: unknown_message, + }, + }); + } + return; + } + + if (keyword_finish && content.toLowerCase() === keyword_finish.toLowerCase()) { + const newSessions = await this.clearSessions(instance, remoteJid); + + const typebotData = { + enabled: findTypebot.enabled, + url: url, + typebot: typebot, + expire: expire, + keyword_finish: keyword_finish, + delay_message: delay_message, + unknown_message: unknown_message, + listening_from_me: listening_from_me, + sessions: newSessions, + }; + + this.create(instance, typebotData); + + return; + } + + try { + const version = this.configService.get('TYPEBOT').API_VERSION; + let urlTypebot: string; + let reqData: {}; + if (version === 'latest') { + urlTypebot = `${data.url}/api/v1/sessions/${data.sessionId}/continueChat`; + reqData = { + message: content, + }; + } else { + urlTypebot = `${data.url}/api/v1/sendMessage`; + reqData = { + message: content, + sessionId: data.sessionId, + }; + } + + const request = await axios.post(urlTypebot, reqData); + + await this.sendWAMessage( + instance, + remoteJid, + request.data.messages, + request.data.input, + request.data.clientSideActions, + ); + } catch (error) { + this.logger.error(error); + return; + } + } + + return; + } + } + + if (session && session.status !== 'opened') { + return; + } + + if (!session) { const data = await this.createNewSession(instance, { + enabled: findTypebot.enabled, url: url, typebot: typebot, expire: expire, @@ -484,7 +657,7 @@ export class TypebotService { sessions.splice(sessions.indexOf(session), 1); const typebotData = { - enabled: true, + enabled: findTypebot.enabled, url: url, typebot: typebot, expire: expire, @@ -500,148 +673,48 @@ export class TypebotService { return; } - const reqData = { - message: content, - sessionId: data.sessionId, - }; + let request: any; + try { + const version = this.configService.get('TYPEBOT').API_VERSION; + let urlTypebot: string; + let reqData: {}; + if (version === 'latest') { + urlTypebot = `${data.url}/api/v1/sessions/${data.sessionId}/continueChat`; + reqData = { + message: content, + }; + } else { + urlTypebot = `${data.url}/api/v1/sendMessage`; + reqData = { + message: content, + sessionId: data.sessionId, + }; + } + request = await axios.post(urlTypebot, reqData); - const request = await axios.post(url + '/api/v1/sendMessage', reqData); - - console.log('request', request); - await this.sendWAMessage( - instance, - remoteJid, - request.data.messages, - request.data.input, - request.data.clientSideActions, - ); + await this.sendWAMessage( + instance, + remoteJid, + request.data.messages, + request.data.input, + request.data.clientSideActions, + ); + } catch (error) { + this.logger.error(error); + return; + } } - return; } - } - if (session && session.status !== 'opened') { - return; - } - - if (!session) { - const data = await this.createNewSession(instance, { - url: url, - typebot: typebot, - expire: expire, - keyword_finish: keyword_finish, - delay_message: delay_message, - unknown_message: unknown_message, - listening_from_me: listening_from_me, - sessions: sessions, - remoteJid: remoteJid, - pushName: msg.pushName, + sessions.map((session) => { + if (session.remoteJid === remoteJid) { + session.updateAt = Date.now(); + } }); - await this.sendWAMessage(instance, remoteJid, data.messages, data.input, data.clientSideActions); - - if (data.messages.length === 0) { - const content = this.getConversationMessage(msg.message); - - if (!content) { - if (unknown_message) { - this.waMonitor.waInstances[instance.instanceName].textMessage({ - number: remoteJid.split('@')[0], - options: { - delay: delay_message || 1000, - presence: 'composing', - }, - textMessage: { - text: unknown_message, - }, - }); - } - return; - } - - if (keyword_finish && content.toLowerCase() === keyword_finish.toLowerCase()) { - sessions.splice(sessions.indexOf(session), 1); - - const typebotData = { - enabled: true, - url: url, - typebot: typebot, - expire: expire, - keyword_finish: keyword_finish, - delay_message: delay_message, - unknown_message: unknown_message, - listening_from_me: listening_from_me, - sessions, - }; - - this.create(instance, typebotData); - - return; - } - - const reqData = { - message: content, - sessionId: data.sessionId, - }; - - const request = await axios.post(url + '/api/v1/sendMessage', reqData); - - console.log('request', request); - await this.sendWAMessage( - instance, - remoteJid, - request.data.messages, - request.data.input, - request.data.clientSideActions, - ); - } - return; - } - - sessions.map((session) => { - if (session.remoteJid === remoteJid) { - session.updateAt = Date.now(); - } - }); - - const typebotData = { - enabled: true, - url: url, - typebot: typebot, - expire: expire, - keyword_finish: keyword_finish, - delay_message: delay_message, - unknown_message: unknown_message, - listening_from_me: listening_from_me, - sessions, - }; - - this.create(instance, typebotData); - - const content = this.getConversationMessage(msg.message); - - if (!content) { - if (unknown_message) { - this.waMonitor.waInstances[instance.instanceName].textMessage({ - number: remoteJid.split('@')[0], - options: { - delay: delay_message || 1000, - presence: 'composing', - }, - textMessage: { - text: unknown_message, - }, - }); - } - return; - } - - if (keyword_finish && content.toLowerCase() === keyword_finish.toLowerCase()) { - sessions.splice(sessions.indexOf(session), 1); - const typebotData = { - enabled: true, + enabled: findTypebot.enabled, url: url, typebot: typebot, expire: expire, @@ -654,24 +727,73 @@ export class TypebotService { this.create(instance, typebotData); + const content = this.getConversationMessage(msg.message); + + if (!content) { + if (unknown_message) { + this.waMonitor.waInstances[instance.instanceName].textMessage({ + number: remoteJid.split('@')[0], + options: { + delay: delay_message || 1000, + presence: 'composing', + }, + textMessage: { + text: unknown_message, + }, + }); + } + return; + } + + if (keyword_finish && content.toLowerCase() === keyword_finish.toLowerCase()) { + sessions.splice(sessions.indexOf(session), 1); + + const typebotData = { + enabled: findTypebot.enabled, + url: url, + typebot: typebot, + expire: expire, + keyword_finish: keyword_finish, + delay_message: delay_message, + unknown_message: unknown_message, + listening_from_me: listening_from_me, + sessions, + }; + + this.create(instance, typebotData); + + return; + } + + const version = this.configService.get('TYPEBOT').API_VERSION; + let urlTypebot: string; + let reqData: {}; + if (version === 'latest') { + urlTypebot = `${url}/api/v1/sessions/${session.sessionId.split('-')[1]}/continueChat`; + reqData = { + message: content, + }; + } else { + urlTypebot = `${url}/api/v1/sendMessage`; + reqData = { + message: content, + sessionId: session.sessionId.split('-')[1], + }; + } + const request = await axios.post(urlTypebot, reqData); + + await this.sendWAMessage( + instance, + remoteJid, + request.data.messages, + request.data.input, + request.data.clientSideActions, + ); + + return; + } catch (error) { + this.logger.error(error); return; } - - const reqData = { - message: content, - sessionId: session.sessionId.split('-')[1], - }; - - const request = await axios.post(url + '/api/v1/sendMessage', reqData); - - await this.sendWAMessage( - instance, - remoteJid, - request.data.messages, - request.data.input, - request.data.clientSideActions, - ); - - return; } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index c4d25c88..618aa65b 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -17,6 +17,7 @@ import makeWASocket, { getContentType, getDevice, GroupMetadata, + isJidBroadcast, isJidGroup, isJidUser, makeCacheableSignalKeyStore, @@ -60,6 +61,7 @@ import { Log, QrCode, Redis, + Sqs, Webhook, Websocket, } from '../../config/env.config'; @@ -70,6 +72,7 @@ import { getAMQP, removeQueues } from '../../libs/amqp.server'; import { dbserver } from '../../libs/db.connect'; import { RedisCache } from '../../libs/redis.client'; import { getIO } from '../../libs/socket.server'; +import { getSQS, removeQueues as removeQueuesSQS } from '../../libs/sqs.server'; import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; import { @@ -81,6 +84,7 @@ import { OnWhatsAppDto, PrivacySettingDto, ReadMessageDto, + SendPresenceDto, WhatsAppNumberDto, } from '../dto/chat.dto'; import { @@ -113,7 +117,7 @@ import { SendTextDto, StatusMessage, } from '../dto/sendMessage.dto'; -import { ChamaaiRaw, ProxyRaw, RabbitmqRaw, SettingsRaw, TypebotRaw } from '../models'; +import { ChamaaiRaw, ProxyRaw, RabbitmqRaw, SettingsRaw, SqsRaw, TypebotRaw } from '../models'; import { ChatRaw } from '../models/chat.model'; import { ChatwootRaw } from '../models/chatwoot.model'; import { ContactRaw } from '../models/contact.model'; @@ -128,9 +132,7 @@ import { Events, MessageSubtype, TypeMediaMessage, wa } from '../types/wa.types' import { waMonitor } from '../whatsapp.module'; import { ChamaaiService } from './chamaai.service'; import { ChatwootService } from './chatwoot.service'; -//import { SocksProxyAgent } from './socks-proxy-agent'; import { TypebotService } from './typebot.service'; - export class WAStartupService { constructor( private readonly configService: ConfigService, @@ -151,6 +153,7 @@ export class WAStartupService { private readonly localSettings: wa.LocalSettings = {}; private readonly localWebsocket: wa.LocalWebsocket = {}; private readonly localRabbitmq: wa.LocalRabbitmq = {}; + private readonly localSqs: wa.LocalSqs = {}; public readonly localTypebot: wa.LocalTypebot = {}; private readonly localProxy: wa.LocalProxy = {}; private readonly localChamaai: wa.LocalChamaai = {}; @@ -163,9 +166,9 @@ export class WAStartupService { private phoneNumber: string; - private chatwootService = new ChatwootService(waMonitor, this.configService); + private chatwootService = new ChatwootService(waMonitor, this.configService, this.repository); - private typebotService = new TypebotService(waMonitor); + private typebotService = new TypebotService(waMonitor, this.configService); private chamaaiService = new ChamaaiService(waMonitor, this.configService); @@ -208,6 +211,7 @@ export class WAStartupService { public async getProfileName() { this.logger.verbose('Getting profile name'); + let profileName = this.client.user?.name ?? this.client.user?.verifiedName; if (!profileName) { this.logger.verbose('Profile name not found, trying to get from database'); @@ -302,7 +306,14 @@ export class WAStartupService { this.logger.verbose(`Webhook url: ${data.url}`); this.logger.verbose(`Webhook events: ${data.events}`); - return data; + + return { + enabled: data.enabled, + url: data.url, + events: data.events, + webhook_by_events: data.webhook_by_events, + webhook_base64: data.webhook_base64, + }; } private async loadChatwoot() { @@ -370,7 +381,16 @@ export class WAStartupService { this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); - return data; + return { + enabled: data.enabled, + account_id: data.account_id, + token: data.token, + url: data.url, + name_inbox: data.name_inbox, + sign_msg: data.sign_msg, + reopen_conversation: data.reopen_conversation, + conversation_pending: data.conversation_pending, + }; } private async loadSettings() { @@ -427,7 +447,14 @@ export class WAStartupService { this.logger.verbose(`Settings always_online: ${data.always_online}`); this.logger.verbose(`Settings read_messages: ${data.read_messages}`); this.logger.verbose(`Settings read_status: ${data.read_status}`); - return data; + return { + reject_call: data.reject_call, + msg_call: data.msg_call, + groups_ignore: data.groups_ignore, + always_online: data.always_online, + read_messages: data.read_messages, + read_status: data.read_status, + }; } private async loadWebsocket() { @@ -461,7 +488,10 @@ export class WAStartupService { } this.logger.verbose(`Websocket events: ${data.events}`); - return data; + return { + enabled: data.enabled, + events: data.events, + }; } private async loadRabbitmq() { @@ -495,7 +525,10 @@ export class WAStartupService { } this.logger.verbose(`Rabbitmq events: ${data.events}`); - return data; + return { + enabled: data.enabled, + events: data.events, + }; } public async removeRabbitmqQueues() { @@ -506,6 +539,51 @@ export class WAStartupService { } } + private async loadSqs() { + this.logger.verbose('Loading sqs'); + const data = await this.repository.sqs.find(this.instanceName); + + this.localSqs.enabled = data?.enabled; + this.logger.verbose(`Sqs enabled: ${this.localSqs.enabled}`); + + this.localSqs.events = data?.events; + this.logger.verbose(`Sqs events: ${this.localSqs.events}`); + + this.logger.verbose('Sqs loaded'); + } + + public async setSqs(data: SqsRaw) { + this.logger.verbose('Setting sqs'); + await this.repository.sqs.create(data, this.instanceName); + this.logger.verbose(`Sqs events: ${data.events}`); + Object.assign(this.localSqs, data); + this.logger.verbose('Sqs set'); + } + + public async findSqs() { + this.logger.verbose('Finding sqs'); + const data = await this.repository.sqs.find(this.instanceName); + + if (!data) { + this.logger.verbose('Sqs not found'); + throw new NotFoundException('Sqs not found'); + } + + this.logger.verbose(`Sqs events: ${data.events}`); + return { + enabled: data.enabled, + events: data.events, + }; + } + + public async removeSqsQueues() { + this.logger.verbose('Removing sqs'); + + if (this.localSqs.enabled) { + removeQueuesSQS(this.instanceName, this.localSqs.events); + } + } + private async loadTypebot() { this.logger.verbose('Loading typebot'); const data = await this.repository.typebot.find(this.instanceName); @@ -561,7 +639,17 @@ export class WAStartupService { throw new NotFoundException('Typebot not found'); } - return data; + return { + enabled: data.enabled, + url: data.url, + typebot: data.typebot, + expire: data.expire, + keyword_finish: data.keyword_finish, + delay_message: data.delay_message, + unknown_message: data.unknown_message, + listening_from_me: data.listening_from_me, + sessions: data.sessions, + }; } private async loadProxy() { @@ -577,14 +665,16 @@ export class WAStartupService { this.logger.verbose('Proxy loaded'); } - public async setProxy(data: ProxyRaw) { + public async setProxy(data: ProxyRaw, reload = true) { this.logger.verbose('Setting proxy'); await this.repository.proxy.create(data, this.instanceName); this.logger.verbose(`Proxy proxy: ${data.proxy}`); Object.assign(this.localProxy, data); this.logger.verbose('Proxy set'); - this.client?.ws?.close(); + if (reload) { + this.reloadConnection(); + } } public async findProxy() { @@ -596,7 +686,10 @@ export class WAStartupService { throw new NotFoundException('Proxy not found'); } - return data; + return { + enabled: data.enabled, + proxy: data.proxy, + }; } private async loadChamaai() { @@ -642,7 +735,13 @@ export class WAStartupService { throw new NotFoundException('Chamaai not found'); } - return data; + return { + enabled: data.enabled, + url: data.url, + token: data.token, + waNumber: data.waNumber, + answerByAudio: data.answerByAudio, + }; } public async sendDataWebhook(event: Events, data: T, local = true) { @@ -650,6 +749,7 @@ export class WAStartupService { const webhookLocal = this.localWebhook.events; const websocketLocal = this.localWebsocket.events; const rabbitmqLocal = this.localRabbitmq.events; + const sqsLocal = this.localSqs.events; const serverUrl = this.configService.get('SERVER').URL; const we = event.replace(/[.-]/gm, '_').toUpperCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase(); @@ -722,6 +822,76 @@ export class WAStartupService { } } + if (this.localSqs.enabled) { + const sqs = getSQS(); + + if (sqs) { + if (Array.isArray(sqsLocal) && sqsLocal.includes(we)) { + const eventFormatted = `${event.replace('.', '_').toLowerCase()}`; + + const queueName = `${this.instanceName}_${eventFormatted}.fifo`; + + const sqsConfig = this.configService.get('SQS'); + + const sqsUrl = `https://sqs.${sqsConfig.REGION}.amazonaws.com/${sqsConfig.ACCOUNT_ID}/${queueName}`; + + const message = { + event, + instance: this.instance.name, + data, + server_url: serverUrl, + date_time: now, + sender: this.wuid, + }; + + if (expose && instanceApikey) { + message['apikey'] = instanceApikey; + } + + const params = { + MessageBody: JSON.stringify(message), + MessageGroupId: 'evolution', + MessageDeduplicationId: `${this.instanceName}_${eventFormatted}_${Date.now()}`, + QueueUrl: sqsUrl, + }; + + sqs.sendMessage(params, (err, data) => { + if (err) { + this.logger.error({ + local: WAStartupService.name + '.sendData-SQS', + message: err?.message, + hostName: err?.hostname, + code: err?.code, + stack: err?.stack, + name: err?.name, + url: queueName, + server_url: serverUrl, + }); + } else { + if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { + const logData = { + local: WAStartupService.name + '.sendData-SQS', + event, + instance: this.instance.name, + data, + server_url: serverUrl, + apikey: (expose && instanceApikey) || null, + date_time: now, + sender: this.wuid, + }; + + if (expose && instanceApikey) { + logData['apikey'] = instanceApikey; + } + + this.logger.log(logData); + } + } + }); + } + } + } + if (this.configService.get('WEBSOCKET')?.ENABLED && this.localWebsocket.enabled) { this.logger.verbose('Sending data to websocket on channel: ' + this.instance.name); if (Array.isArray(websocketLocal) && websocketLocal.includes(we)) { @@ -1167,6 +1337,7 @@ export class WAStartupService { this.loadSettings(); this.loadWebsocket(); this.loadRabbitmq(); + this.loadSqs(); this.loadTypebot(); this.loadProxy(); this.loadChamaai(); @@ -1182,11 +1353,22 @@ export class WAStartupService { let options; if (this.localProxy.enabled) { - this.logger.verbose('Proxy enabled'); - options = { - agent: new ProxyAgent(this.localProxy.proxy as any), - fetchAgent: new ProxyAgent(this.localProxy.proxy as any), - }; + this.logger.info('Proxy enabled: ' + this.localProxy.proxy); + + if (this.localProxy.proxy.includes('proxyscrape')) { + const response = await axios.get(this.localProxy.proxy); + const text = response.data; + const proxyUrls = text.split('\r\n'); + const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length)); + const proxyUrl = 'http://' + proxyUrls[rand]; + options = { + agent: new ProxyAgent(proxyUrl as any), + }; + } else { + options = { + agent: new ProxyAgent(this.localProxy.proxy as any), + }; + } } const socketConfig: UserFacingSocketConfig = { @@ -1200,16 +1382,23 @@ export class WAStartupService { browser, version, markOnlineOnConnect: this.localSettings.always_online, + retryRequestDelayMs: 10, connectTimeoutMs: 60_000, qrTimeout: 40_000, defaultQueryTimeoutMs: undefined, emitOwnEvents: false, + shouldIgnoreJid: (jid) => { + const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid); + const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid); + + return isGroupJid || isBroadcast; + }, msgRetryCounterCache: this.msgRetryCounterCache, getMessage: async (key) => (await this.getMessage(key)) as Promise, generateHighQualityLinkPreview: true, - syncFullHistory: true, + syncFullHistory: false, userDevicesCache: this.userDevicesCache, - transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, + transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, patchMessageBeforeSending: (message) => { const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); if (requiresPatch) { @@ -1280,16 +1469,23 @@ export class WAStartupService { browser, version, markOnlineOnConnect: this.localSettings.always_online, + retryRequestDelayMs: 10, connectTimeoutMs: 60_000, qrTimeout: 40_000, defaultQueryTimeoutMs: undefined, emitOwnEvents: false, + shouldIgnoreJid: (jid) => { + const isGroupJid = this.localSettings.groups_ignore && isJidGroup(jid); + const isBroadcast = !this.localSettings.read_status && isJidBroadcast(jid); + + return isGroupJid || isBroadcast; + }, msgRetryCounterCache: this.msgRetryCounterCache, getMessage: async (key) => (await this.getMessage(key)) as Promise, generateHighQualityLinkPreview: true, - syncFullHistory: true, + syncFullHistory: false, userDevicesCache: this.userDevicesCache, - transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, + transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 }, patchMessageBeforeSending: (message) => { const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); if (requiresPatch) { @@ -1339,10 +1535,10 @@ export class WAStartupService { } this.logger.verbose('Sending data to webhook in event CHATS_UPSERT'); - await this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); + this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); this.logger.verbose('Inserting chats in database'); - await this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); + this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); }, 'chats.update': async ( @@ -1360,7 +1556,7 @@ export class WAStartupService { }); this.logger.verbose('Sending data to webhook in event CHATS_UPDATE'); - await this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); + this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); }, 'chats.delete': async (chats: string[]) => { @@ -1375,7 +1571,7 @@ export class WAStartupService { ); this.logger.verbose('Sending data to webhook in event CHATS_DELETE'); - await this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); + this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); }, }; @@ -1404,10 +1600,10 @@ export class WAStartupService { } this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); - await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); + this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); this.logger.verbose('Inserting contacts in database'); - await this.repository.contact.insert(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); + this.repository.contact.insert(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); }, 'contacts.update': async (contacts: Partial[], database: Database) => { @@ -1425,10 +1621,10 @@ export class WAStartupService { } this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); - await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); + this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); this.logger.verbose('Updating contacts in database'); - await this.repository.contact.update(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); + this.repository.contact.update(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); }, }; @@ -1458,10 +1654,10 @@ export class WAStartupService { }); this.logger.verbose('Sending data to webhook in event CHATS_SET'); - await this.sendDataWebhook(Events.CHATS_SET, chatsRaw); + this.sendDataWebhook(Events.CHATS_SET, chatsRaw); this.logger.verbose('Inserting chats in database'); - await this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); + this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); } const messagesRaw: MessageRaw[] = []; @@ -1508,160 +1704,170 @@ export class WAStartupService { database: Database, settings: SettingsRaw, ) => { - this.logger.verbose('Event received: messages.upsert'); - const received = messages[0]; + try { + this.logger.verbose('Event received: messages.upsert'); + for (const received of messages) { + if ( + (type !== 'notify' && type !== 'append') || + received.message?.protocolMessage || + received.message?.pollUpdateMessage + ) { + this.logger.verbose('message rejected'); + return; + } - if ( - type !== 'notify' || - !received?.message || - received.message?.protocolMessage || - received.message.senderKeyDistributionMessage || - received.message?.pollUpdateMessage - ) { - this.logger.verbose('message rejected'); - return; - } + if (Long.isLong(received.messageTimestamp)) { + received.messageTimestamp = received.messageTimestamp?.toNumber(); + } - if (Long.isLong(received.messageTimestamp)) { - received.messageTimestamp = received.messageTimestamp?.toNumber(); - } + if (settings?.groups_ignore && received.key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } - if (settings?.groups_ignore && received.key.remoteJid.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } + let messageRaw: MessageRaw; - let messageRaw: MessageRaw; + if ( + (this.localWebhook.webhook_base64 === true && received?.message.documentMessage) || + received?.message?.imageMessage + ) { + const buffer = await downloadMediaMessage( + { key: received.key, message: received?.message }, + 'buffer', + {}, + { + logger: P({ level: 'error' }) as any, + reuploadRequest: this.client.updateMediaMessage, + }, + ); + messageRaw = { + key: received.key, + pushName: received.pushName, + message: { + ...received.message, + base64: buffer ? buffer.toString('base64') : undefined, + }, + messageType: getContentType(received.message), + messageTimestamp: received.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(received.key.id), + }; + } else { + messageRaw = { + key: received.key, + pushName: received.pushName, + message: { ...received.message }, + messageType: getContentType(received.message), + messageTimestamp: received.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(received.key.id), + }; + } - if ( - (this.localWebhook.webhook_base64 === true && received?.message.documentMessage) || - received?.message.imageMessage - ) { - const buffer = await downloadMediaMessage( - { key: received.key, message: received?.message }, - 'buffer', - {}, - { - logger: P({ level: 'error' }) as any, - reuploadRequest: this.client.updateMediaMessage, - }, - ); - console.log(buffer); - messageRaw = { - key: received.key, - pushName: received.pushName, - message: { - ...received.message, - base64: buffer ? buffer.toString('base64') : undefined, - }, - messageType: getContentType(received.message), - messageTimestamp: received.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(received.key.id), - }; - } else { - messageRaw = { - key: received.key, - pushName: received.pushName, - message: { ...received.message }, - messageType: getContentType(received.message), - messageTimestamp: received.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(received.key.id), - }; - } + if (this.localSettings.read_messages && received.key.id !== 'status@broadcast') { + await this.client.readMessages([received.key]); + } - if (this.localSettings.read_messages && received.key.id !== 'status@broadcast') { - await this.client.readMessages([received.key]); - } + if (this.localSettings.read_status && received.key.id === 'status@broadcast') { + await this.client.readMessages([received.key]); + } - if (this.localSettings.read_status && received.key.id === 'status@broadcast') { - await this.client.readMessages([received.key]); - } + this.logger.log(messageRaw); - this.logger.log(messageRaw); + this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); + this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); - this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); - await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + if (this.localChatwoot.enabled && !received.key.id.includes('@broadcast')) { + const chatwootSentMessage = await this.chatwootService.eventWhatsapp( + Events.MESSAGES_UPSERT, + { instanceName: this.instance.name }, + messageRaw, + ); - if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( - Events.MESSAGES_UPSERT, - { instanceName: this.instance.name }, - messageRaw, - ); - } + if (chatwootSentMessage?.id) { + messageRaw.chatwootMessageId = chatwootSentMessage.id; + } + } - if (this.localTypebot.enabled) { - if (!(this.localTypebot.listening_from_me === false && messageRaw.key.fromMe === true)) { - await this.typebotService.sendTypebot( - { instanceName: this.instance.name }, - messageRaw.key.remoteJid, - messageRaw, + const typebotSessionRemoteJid = this.localTypebot.sessions?.find( + (session) => session.remoteJid === received.key.remoteJid, ); + + if ((this.localTypebot.enabled && type === 'notify') || typebotSessionRemoteJid) { + if (!(this.localTypebot.listening_from_me === false && messageRaw.key.fromMe === true)) { + if (messageRaw.messageType !== 'reactionMessage') + await this.typebotService.sendTypebot( + { instanceName: this.instance.name }, + messageRaw.key.remoteJid, + messageRaw, + ); + } + } + + if (this.localChamaai.enabled && messageRaw.key.fromMe === false && type === 'notify') { + await this.chamaaiService.sendChamaai( + { instanceName: this.instance.name }, + messageRaw.key.remoteJid, + messageRaw, + ); + } + + this.logger.verbose('Inserting message in database'); + await this.repository.message.insert([messageRaw], this.instance.name, database.SAVE_DATA.NEW_MESSAGE); + + this.logger.verbose('Verifying contact from message'); + const contact = await this.repository.contact.find({ + where: { owner: this.instance.name, id: received.key.remoteJid }, + }); + + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: received.pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + owner: this.instance.name, + }; + + if (contactRaw.id === 'status@broadcast') { + this.logger.verbose('Contact is status@broadcast'); + return; + } + + if (contact?.length) { + this.logger.verbose('Contact found in database'); + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: contact[0].pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + owner: this.instance.name, + }; + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); + + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.CONTACTS_UPDATE, + { instanceName: this.instance.name }, + contactRaw, + ); + } + + this.logger.verbose('Updating contact in database'); + await this.repository.contact.update([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); + return; + } + + this.logger.verbose('Contact not found in database'); + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); + + this.logger.verbose('Inserting contact in database'); + this.repository.contact.insert([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); } + } catch (error) { + this.logger.error(error); } - - if (this.localChamaai.enabled && messageRaw.key.fromMe === false) { - await this.chamaaiService.sendChamaai( - { instanceName: this.instance.name }, - messageRaw.key.remoteJid, - messageRaw, - ); - } - - this.logger.verbose('Inserting message in database'); - await this.repository.message.insert([messageRaw], this.instance.name, database.SAVE_DATA.NEW_MESSAGE); - - this.logger.verbose('Verifying contact from message'); - const contact = await this.repository.contact.find({ - where: { owner: this.instance.name, id: received.key.remoteJid }, - }); - - const contactRaw: ContactRaw = { - id: received.key.remoteJid, - pushName: received.pushName, - profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, - owner: this.instance.name, - }; - - if (contactRaw.id === 'status@broadcast') { - this.logger.verbose('Contact is status@broadcast'); - return; - } - - if (contact?.length) { - this.logger.verbose('Contact found in database'); - const contactRaw: ContactRaw = { - id: received.key.remoteJid, - pushName: contact[0].pushName, - profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, - owner: this.instance.name, - }; - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); - await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); - - if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( - Events.CONTACTS_UPDATE, - { instanceName: this.instance.name }, - contactRaw, - ); - } - - this.logger.verbose('Updating contact in database'); - await this.repository.contact.update([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); - return; - } - - this.logger.verbose('Contact not found in database'); - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); - await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); - - this.logger.verbose('Inserting contact in database'); - await this.repository.contact.insert([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); }, 'messages.update': async (args: WAMessageUpdate[], database: Database, settings: SettingsRaw) => { @@ -1705,7 +1911,7 @@ export class WAStartupService { this.logger.verbose('Message deleted'); this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); - await this.sendDataWebhook(Events.MESSAGES_DELETE, key); + this.sendDataWebhook(Events.MESSAGES_DELETE, key); const message: MessageUpdateRaw = { ...key, @@ -1736,10 +1942,10 @@ export class WAStartupService { this.logger.verbose(message); this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); - await this.sendDataWebhook(Events.MESSAGES_UPDATE, message); + this.sendDataWebhook(Events.MESSAGES_UPDATE, message); this.logger.verbose('Inserting message in database'); - await this.repository.messageUpdate.insert([message], this.instance.name, database.SAVE_DATA.MESSAGE_UPDATE); + this.repository.messageUpdate.insert([message], this.instance.name, database.SAVE_DATA.MESSAGE_UPDATE); } } }, @@ -1788,7 +1994,7 @@ export class WAStartupService { this.client.rejectCall(call.id, call.from); } - if (settings?.msg_call.trim().length > 0 && call.status == 'offer') { + if (settings?.msg_call?.trim().length > 0 && call.status == 'offer') { this.logger.verbose('Sending message in call'); const msg = await this.client.sendMessage(call.from, { text: settings.msg_call, @@ -1934,8 +2140,8 @@ export class WAStartupService { private createJid(number: string): string { this.logger.verbose('Creating jid with number: ' + number); - if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { - this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); + if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) { + this.logger.verbose('Number already contains @g.us or @s.whatsapp.net or @lid'); return number; } @@ -2159,6 +2365,20 @@ export class WAStartupService { !message['conversation'] && sender !== 'status@broadcast' ) { + if (message['reactionMessage']) { + this.logger.verbose('Sending reaction'); + return await this.client.sendMessage( + sender, + { + react: { + text: message['reactionMessage']['text'], + key: message['reactionMessage']['key'], + }, + } as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, + ); + } + if (!message['audio']) { this.logger.verbose('Sending message'); return await this.client.sendMessage( @@ -2174,7 +2394,6 @@ export class WAStartupService { ); } } - if (message['conversation']) { this.logger.verbose('Sending message'); return await this.client.sendMessage( @@ -2222,7 +2441,7 @@ export class WAStartupService { this.logger.log(messageRaw); this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); - await this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); + this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); if (this.localChatwoot.enabled && !isChatwoot) { this.chatwootService.eventWhatsapp(Events.SEND_MESSAGE, { instanceName: this.instance.name }, messageRaw); @@ -2248,6 +2467,38 @@ export class WAStartupService { return this.stateConnection; } + public async sendPresence(data: SendPresenceDto) { + try { + const { number } = data; + + this.logger.verbose(`Check if number "${number}" is WhatsApp`); + const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift(); + + this.logger.verbose(`Exists: "${isWA.exists}" | jid: ${isWA.jid}`); + if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { + throw new BadRequestException(isWA); + } + + const sender = isWA.jid; + + this.logger.verbose('Sending presence'); + await this.client.presenceSubscribe(sender); + this.logger.verbose('Subscribing to presence'); + + await this.client.sendPresenceUpdate(data.options?.presence ?? 'composing', sender); + this.logger.verbose('Sending presence update: ' + data.options?.presence ?? 'composing'); + + await delay(data.options.delay); + this.logger.verbose('Set delay: ' + data.options.delay); + + await this.client.sendPresenceUpdate('paused', sender); + this.logger.verbose('Sending presence update: paused'); + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + // Send Message Controller public async textMessage(data: SendTextDto, isChatwoot = false) { this.logger.verbose('Sending text message'); @@ -2434,10 +2685,14 @@ export class WAStartupService { let mimetype: string; - if (isURL(mediaMessage.media)) { - mimetype = getMIMEType(mediaMessage.media); + if (mediaMessage.mimetype) { + mimetype = mediaMessage.mimetype; } else { - mimetype = getMIMEType(mediaMessage.fileName); + if (isURL(mediaMessage.media)) { + mimetype = getMIMEType(mediaMessage.media); + } else { + mimetype = getMIMEType(mediaMessage.fileName); + } } this.logger.verbose('Mimetype: ' + mimetype); @@ -3062,7 +3317,16 @@ export class WAStartupService { public async fetchPrivacySettings() { this.logger.verbose('Fetching privacy settings'); - return await this.client.fetchPrivacySettings(); + const privacy = await this.client.fetchPrivacySettings(); + + return { + readreceipts: privacy.readreceipts, + profile: privacy.profile, + status: privacy.status, + online: privacy.online, + last: privacy.last, + groupadd: privacy.groupadd, + }; } public async updatePrivacySettings(settings: PrivacySettingDto) { @@ -3086,7 +3350,7 @@ export class WAStartupService { await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); this.logger.verbose('Groups add privacy updated'); - this.client?.ws?.close(); + this.reloadConnection(); return { update: 'success', @@ -3174,9 +3438,12 @@ export class WAStartupService { } else { throw new BadRequestException('"profilePicture" must be a url or a base64'); } + await this.client.updateProfilePicture(this.instance.wuid, pic); this.logger.verbose('Profile picture updated'); + this.reloadConnection(); + return { update: 'success' }; } catch (error) { throw new InternalServerErrorException('Error updating profile picture', error.toString()); @@ -3188,6 +3455,8 @@ export class WAStartupService { try { await this.client.removeProfilePicture(this.instance.wuid); + this.reloadConnection(); + return { update: 'success' }; } catch (error) { throw new InternalServerErrorException('Error removing profile picture', error.toString()); @@ -3302,7 +3571,7 @@ export class WAStartupService { subject: group.subject, subjectOwner: group.subjectOwner, subjectTime: group.subjectTime, - size: group.size, + size: group.participants.length, creation: group.creation, owner: group.owner, desc: group.desc, diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 9f326c8a..27582001 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -84,6 +84,11 @@ export declare namespace wa { events?: string[]; }; + export type LocalSqs = { + enabled?: boolean; + events?: string[]; + }; + type Session = { remoteJid?: string; sessionId?: string; diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index a37e98ef..1fb84de6 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -12,8 +12,8 @@ import { ProxyController } from './controllers/proxy.controller'; import { RabbitmqController } from './controllers/rabbitmq.controller'; import { SendMessageController } from './controllers/sendMessage.controller'; import { SettingsController } from './controllers/settings.controller'; +import { SqsController } from './controllers/sqs.controller'; import { TypebotController } from './controllers/typebot.controller'; -import { ViewsController } from './controllers/views.controller'; import { WebhookController } from './controllers/webhook.controller'; import { WebsocketController } from './controllers/websocket.controller'; import { @@ -27,6 +27,7 @@ import { ProxyModel, RabbitmqModel, SettingsModel, + SqsModel, TypebotModel, WebhookModel, WebsocketModel, @@ -42,6 +43,7 @@ import { ProxyRepository } from './repository/proxy.repository'; import { RabbitmqRepository } from './repository/rabbitmq.repository'; import { RepositoryBroker } from './repository/repository.manager'; import { SettingsRepository } from './repository/settings.repository'; +import { SqsRepository } from './repository/sqs.repository'; import { TypebotRepository } from './repository/typebot.repository'; import { WebhookRepository } from './repository/webhook.repository'; import { WebsocketRepository } from './repository/websocket.repository'; @@ -52,6 +54,7 @@ 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'; import { TypebotService } from './services/typebot.service'; import { WebhookService } from './services/webhook.service'; import { WebsocketService } from './services/websocket.service'; @@ -68,6 +71,7 @@ const websocketRepository = new WebsocketRepository(WebsocketModel, configServic const proxyRepository = new ProxyRepository(ProxyModel, configService); const chamaaiRepository = new ChamaaiRepository(ChamaaiModel, configService); const rabbitmqRepository = new RabbitmqRepository(RabbitmqModel, configService); +const sqsRepository = new SqsRepository(SqsModel, configService); const chatwootRepository = new ChatwootRepository(ChatwootModel, configService); const settingsRepository = new SettingsRepository(SettingsModel, configService); const authRepository = new AuthRepository(AuthModel, configService); @@ -82,6 +86,7 @@ export const repository = new RepositoryBroker( settingsRepository, websocketRepository, rabbitmqRepository, + sqsRepository, typebotRepository, proxyRepository, chamaaiRepository, @@ -96,7 +101,7 @@ export const waMonitor = new WAMonitoringService(eventEmitter, configService, re const authService = new AuthService(configService, waMonitor, repository); -const typebotService = new TypebotService(waMonitor); +const typebotService = new TypebotService(waMonitor, configService); export const typebotController = new TypebotController(typebotService); @@ -120,9 +125,13 @@ const rabbitmqService = new RabbitmqService(waMonitor); export const rabbitmqController = new RabbitmqController(rabbitmqService); -const chatwootService = new ChatwootService(waMonitor, configService); +const sqsService = new SqsService(waMonitor); -export const chatwootController = new ChatwootController(chatwootService, configService); +export const sqsController = new SqsController(sqsService); + +const chatwootService = new ChatwootService(waMonitor, configService, repository); + +export const chatwootController = new ChatwootController(chatwootService, configService, repository); const settingsService = new SettingsService(waMonitor); @@ -139,10 +148,11 @@ export const instanceController = new InstanceController( settingsService, websocketService, rabbitmqService, + proxyService, + sqsService, typebotService, cache, ); -export const viewsController = new ViewsController(waMonitor, configService); export const sendMessageController = new SendMessageController(waMonitor); export const chatController = new ChatController(waMonitor); export const groupController = new GroupController(waMonitor);