Merge branch 'develop' into foqc/develop

This commit is contained in:
Davidson Gomes 2025-07-14 14:46:36 -03:00 committed by GitHub
commit 5d0278a589
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 2437 additions and 2067 deletions

View File

@ -1,7 +1,6 @@
.git .git
*Dockerfile* *Dockerfile*
*docker-compose* *docker-compose*
package-lock.json
.env .env
node_modules node_modules
dist dist

View File

@ -196,7 +196,7 @@ CONFIG_SESSION_PHONE_NAME=Chrome
# Whatsapp Web version for baileys channel # Whatsapp Web version for baileys channel
# https://web.whatsapp.com/check-update?version=0&platform=web # https://web.whatsapp.com/check-update?version=0&platform=web
# CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200
# Set qrcode display limit # Set qrcode display limit
QRCODE_LIMIT=30 QRCODE_LIMIT=30

View File

@ -1,3 +1,14 @@
# 2.3.1 (develop)
### Fixed
* Update Baileys Version
* Update Dockerhub Repository and Delete Config Session Variable
* Fixed sending variables in typebot
* Add unreadMessages in the response
* Phone number as message ID for Evo AI
* Fix upload to s3 when media message
# 2.3.0 (2025-06-17 09:19) # 2.3.0 (2025-06-17 09:19)
### Feature ### Feature

View File

@ -2,7 +2,7 @@ version: "3.7"
services: services:
evolution_v2: evolution_v2:
image: atendai/evolution-api:v2.2.3 image: evoapicloud/evolution-api:v2.3.1
volumes: volumes:
- evolution_instances:/evolution/instances - evolution_instances:/evolution/instances
networks: networks:
@ -94,7 +94,7 @@ services:
- WEBHOOK_EVENTS_ERRORS_WEBHOOK= - WEBHOOK_EVENTS_ERRORS_WEBHOOK=
- CONFIG_SESSION_PHONE_CLIENT=Evolution API V2 - CONFIG_SESSION_PHONE_CLIENT=Evolution API V2
- CONFIG_SESSION_PHONE_NAME=Chrome - CONFIG_SESSION_PHONE_NAME=Chrome
- CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200 #- CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200
- QRCODE_LIMIT=30 - QRCODE_LIMIT=30
- OPENAI_ENABLED=true - OPENAI_ENABLED=true
- DIFY_ENABLED=true - DIFY_ENABLED=true

View File

@ -3,15 +3,17 @@ FROM node:20-alpine AS builder
RUN apk update && \ RUN apk update && \
apk add --no-cache git ffmpeg wget curl bash openssl apk add --no-cache git ffmpeg wget curl bash openssl
LABEL version="2.3.0" description="Api to control whatsapp features through http requests." LABEL version="2.3.1" description="Api to control whatsapp features through http requests."
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
LABEL contact="contato@evolution-api.com" LABEL contact="contato@evolution-api.com"
WORKDIR /evolution WORKDIR /evolution
COPY ./package.json ./tsconfig.json ./ COPY ./package*.json ./
COPY ./tsconfig.json ./
COPY ./tsup.config.ts ./
RUN npm install RUN npm ci --silent
COPY ./src ./src COPY ./src ./src
COPY ./public ./public COPY ./public ./public
@ -19,7 +21,6 @@ COPY ./prisma ./prisma
COPY ./manager ./manager COPY ./manager ./manager
COPY ./.env.example ./.env COPY ./.env.example ./.env
COPY ./runWithProvider.js ./ COPY ./runWithProvider.js ./
COPY ./tsup.config.ts ./
COPY ./Docker ./Docker COPY ./Docker ./Docker
@ -35,6 +36,7 @@ RUN apk update && \
apk add tzdata ffmpeg bash openssl apk add tzdata ffmpeg bash openssl
ENV TZ=America/Sao_Paulo ENV TZ=America/Sao_Paulo
ENV DOCKER_ENV=true
WORKDIR /evolution WORKDIR /evolution

View File

@ -117,4 +117,4 @@ Please contact contato@evolution-api.com to inquire about licensing matters.
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).
© 2024 Evolution API © 2025 Evolution API

3625
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "evolution-api", "name": "evolution-api",
"version": "2.3.0", "version": "2.3.1",
"description": "Rest api for communication with WhatsApp", "description": "Rest api for communication with WhatsApp",
"main": "./dist/main.js", "main": "./dist/main.js",
"type": "commonjs", "type": "commonjs",
@ -73,7 +73,7 @@
"form-data": "^4.0.1", "form-data": "^4.0.1",
"https-proxy-agent": "^7.0.6", "https-proxy-agent": "^7.0.6",
"i18next": "^23.7.19", "i18next": "^23.7.19",
"jimp": "^0.16.13", "jimp": "^1.6.0",
"json-schema": "^0.4.0", "json-schema": "^0.4.0",
"jsonschema": "^1.4.1", "jsonschema": "^1.4.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@ -95,9 +95,10 @@
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"redis": "^4.7.0", "redis": "^4.7.0",
"sharp": "^0.32.6", "sharp": "^0.34.2",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"swagger-ui-express": "^5.0.1",
"tsup": "^8.3.5" "tsup": "^8.3.5"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,9 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE `Setting`
ADD COLUMN IF NOT EXISTS `wavoipToken` VARCHAR(100);

View File

@ -652,16 +652,16 @@ model N8n {
webhookUrl String? @db.VarChar(255) webhookUrl String? @db.VarChar(255)
basicAuthUser String? @db.VarChar(255) basicAuthUser String? @db.VarChar(255)
basicAuthPass String? @db.VarChar(255) basicAuthPass String? @db.VarChar(255)
expire Int? @default(0) @db.Integer expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false) @db.Boolean stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false) @db.Boolean keepOpen Boolean? @default(false)
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
ignoreJids Json? ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean splitMessages Boolean? @default(false)
timePerChar Int? @default(50) @db.Integer timePerChar Int? @default(50) @db.Integer
triggerType TriggerType? triggerType TriggerType?
triggerOperator TriggerOperator? triggerOperator TriggerOperator?
@ -675,16 +675,16 @@ model N8n {
model N8nSetting { model N8nSetting {
id String @id @default(cuid()) id String @id @default(cuid())
expire Int? @default(0) @db.Integer expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false) @db.Boolean stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false) @db.Boolean keepOpen Boolean? @default(false)
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
ignoreJids Json? ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean splitMessages Boolean? @default(false)
timePerChar Int? @default(50) @db.Integer timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp
@ -700,16 +700,16 @@ model Evoai {
description String? @db.VarChar(255) description String? @db.VarChar(255)
agentUrl String? @db.VarChar(255) agentUrl String? @db.VarChar(255)
apiKey String? @db.VarChar(255) apiKey String? @db.VarChar(255)
expire Int? @default(0) @db.Integer expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false) @db.Boolean stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false) @db.Boolean keepOpen Boolean? @default(false)
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
ignoreJids Json? ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean splitMessages Boolean? @default(false)
timePerChar Int? @default(50) @db.Integer timePerChar Int? @default(50) @db.Integer
triggerType TriggerType? triggerType TriggerType?
triggerOperator TriggerOperator? triggerOperator TriggerOperator?
@ -723,16 +723,16 @@ model Evoai {
model EvoaiSetting { model EvoaiSetting {
id String @id @default(cuid()) id String @id @default(cuid())
expire Int? @default(0) @db.Integer expire Int? @default(0) @db.Int
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean listeningFromMe Boolean? @default(false)
stopBotFromMe Boolean? @default(false) @db.Boolean stopBotFromMe Boolean? @default(false)
keepOpen Boolean? @default(false) @db.Boolean keepOpen Boolean? @default(false)
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
ignoreJids Json? ignoreJids Json?
splitMessages Boolean? @default(false) @db.Boolean splitMessages Boolean? @default(false)
timePerChar Int? @default(50) @db.Integer timePerChar Int? @default(50) @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp updatedAt DateTime @updatedAt @db.Timestamp

View File

@ -455,6 +455,12 @@ export class EvolutionStartupService extends ChannelStartupService {
if (base64 || file || audioFile) { if (base64 || file || audioFile) {
if (this.configService.get<S3>('S3').ENABLE) { if (this.configService.get<S3>('S3').ENABLE) {
try { try {
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(messageRaw);
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const fileBuffer = audioFile?.buffer || file?.buffer; const fileBuffer = audioFile?.buffer || file?.buffer;
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer; const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
@ -488,6 +494,7 @@ export class EvolutionStartupService extends ChannelStartupService {
const mediaUrl = await s3Service.getObjectUrl(fullName); const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl; messageRaw.message.mediaUrl = mediaUrl;
}
} catch (error) { } catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
} }

View File

@ -429,6 +429,12 @@ export class BusinessStartupService extends ChannelStartupService {
try { try {
const message: any = received; const message: any = received;
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(messageRaw);
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const id = message.messages[0][message.messages[0].type].id; const id = message.messages[0][message.messages[0].type].id;
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL; let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION; const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
@ -533,6 +539,7 @@ export class BusinessStartupService extends ChannelStartupService {
} }
} }
} }
}
} catch (error) { } catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
} }

View File

@ -99,6 +99,7 @@ import makeWASocket, {
Contact, Contact,
delay, delay,
DisconnectReason, DisconnectReason,
downloadContentFromMessage,
downloadMediaMessage, downloadMediaMessage,
generateWAMessageFromContent, generateWAMessageFromContent,
getAggregateVotesInPollMessage, getAggregateVotesInPollMessage,
@ -122,7 +123,7 @@ import makeWASocket, {
WABrowserDescription, WABrowserDescription,
WAMediaUpload, WAMediaUpload,
WAMessage, WAMessage,
WAMessageUpdate, WAMessageKey,
WAPresence, WAPresence,
WASocket, WASocket,
} from 'baileys'; } from 'baileys';
@ -887,7 +888,7 @@ export class BaileysStartupService extends ChannelStartupService {
}: { }: {
chats: Chat[]; chats: Chat[];
contacts: Contact[]; contacts: Contact[];
messages: proto.IWebMessageInfo[]; messages: WAMessage[];
isLatest?: boolean; isLatest?: boolean;
progress?: number; progress?: number;
syncType?: proto.HistorySync.HistorySyncType; syncType?: proto.HistorySync.HistorySyncType;
@ -973,6 +974,10 @@ export class BaileysStartupService extends ChannelStartupService {
continue; continue;
} }
if (m.key.remoteJid?.includes('@lid') && m.key.senderPn) {
m.key.remoteJid = m.key.senderPn;
}
if (Long.isLong(m?.messageTimestamp)) { if (Long.isLong(m?.messageTimestamp)) {
m.messageTimestamp = m.messageTimestamp?.toNumber(); m.messageTimestamp = m.messageTimestamp?.toNumber();
} }
@ -1030,11 +1035,29 @@ export class BaileysStartupService extends ChannelStartupService {
}, },
'messages.upsert': async ( 'messages.upsert': async (
{ messages, type, requestId }: { messages: proto.IWebMessageInfo[]; type: MessageUpsertType; requestId?: string }, { messages, type, requestId }: { messages: WAMessage[]; type: MessageUpsertType; requestId?: string },
settings: any, settings: any,
) => { ) => {
try { try {
for (const received of messages) { for (const received of messages) {
if (received.key.remoteJid?.includes('@lid') && received.key.senderPn) {
(received.key as { previousRemoteJid?: string | null }).previousRemoteJid = received.key.remoteJid;
received.key.remoteJid = received.key.senderPn;
}
if (
received?.messageStubParameters?.some?.((param) =>
[
'No matching sessions found for message',
'Bad MAC',
'failed to decrypt message',
'SessionError',
'Invalid PreKey ID',
].some((err) => param?.includes?.(err)),
)
) {
this.logger.warn(`Message ignored with messageStubParameters: ${JSON.stringify(received, null, 2)}`);
continue;
}
if (received.message?.conversation || received.message?.extendedTextMessage?.text) { if (received.message?.conversation || received.message?.extendedTextMessage?.text) {
const text = received.message?.conversation || received.message?.extendedTextMessage?.text; const text = received.message?.conversation || received.message?.extendedTextMessage?.text;
@ -1226,6 +1249,13 @@ export class BaileysStartupService extends ChannelStartupService {
if (this.configService.get<S3>('S3').ENABLE) { if (this.configService.get<S3>('S3').ENABLE) {
try { try {
const message: any = received; const message: any = received;
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(message);
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const media = await this.getBase64FromMediaMessage({ message }, true); const media = await this.getBase64FromMediaMessage({ message }, true);
const { buffer, mediaType, fileName, size } = media; const { buffer, mediaType, fileName, size } = media;
@ -1253,6 +1283,7 @@ export class BaileysStartupService extends ChannelStartupService {
messageRaw.message.mediaUrl = mediaUrl; messageRaw.message.mediaUrl = mediaUrl;
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
}
} catch (error) { } catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
} }
@ -1356,7 +1387,7 @@ export class BaileysStartupService extends ChannelStartupService {
} }
}, },
'messages.update': async (args: WAMessageUpdate[], settings: any) => { 'messages.update': async (args: { update: Partial<WAMessage>; key: WAMessageKey }[], settings: any) => {
this.logger.log(`Update messages ${JSON.stringify(args, undefined, 2)}`); this.logger.log(`Update messages ${JSON.stringify(args, undefined, 2)}`);
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true} const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
@ -1366,6 +1397,10 @@ export class BaileysStartupService extends ChannelStartupService {
continue; continue;
} }
if (key.remoteJid?.includes('@lid') && key.senderPn) {
key.remoteJid = key.senderPn;
}
const updateKey = `${this.instance.id}_${key.id}_${update.status}`; const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
const cached = await this.baileysCache.get(updateKey); const cached = await this.baileysCache.get(updateKey);
@ -2121,6 +2156,13 @@ export class BaileysStartupService extends ChannelStartupService {
if (isMedia && this.configService.get<S3>('S3').ENABLE) { if (isMedia && this.configService.get<S3>('S3').ENABLE) {
try { try {
const message: any = messageRaw; const message: any = messageRaw;
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(message);
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const media = await this.getBase64FromMediaMessage({ message }, true); const media = await this.getBase64FromMediaMessage({ message }, true);
const { buffer, mediaType, fileName, size } = media; const { buffer, mediaType, fileName, size } = media;
@ -2146,6 +2188,7 @@ export class BaileysStartupService extends ChannelStartupService {
messageRaw.message.mediaUrl = mediaUrl; messageRaw.message.mediaUrl = mediaUrl;
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
}
} catch (error) { } catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
} }
@ -3413,6 +3456,18 @@ export class BaileysStartupService extends ChannelStartupService {
} }
} }
public async mapMediaType(mediaType) {
const map = {
imageMessage: 'image',
videoMessage: 'video',
documentMessage: 'document',
stickerMessage: 'sticker',
audioMessage: 'audio',
ptvMessage: 'video',
};
return map[mediaType] || null;
}
public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) { public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) {
try { try {
const m = data?.message; const m = data?.message;
@ -3437,6 +3492,23 @@ export class BaileysStartupService extends ChannelStartupService {
let mediaMessage: any; let mediaMessage: any;
let mediaType: string; let mediaType: string;
if (msg.message?.templateMessage) {
const template =
msg.message.templateMessage.hydratedTemplate || msg.message.templateMessage.hydratedFourRowTemplate;
for (const type of TypeMediaMessage) {
if (template[type]) {
mediaMessage = template[type];
mediaType = type;
msg.message = { [type]: { ...template[type], url: template[type].staticUrl } };
break;
}
}
if (!mediaMessage) {
throw 'Template message does not contain a supported media type';
}
} else {
for (const type of TypeMediaMessage) { for (const type of TypeMediaMessage) {
mediaMessage = msg.message[type]; mediaMessage = msg.message[type];
if (mediaMessage) { if (mediaMessage) {
@ -3448,17 +3520,48 @@ export class BaileysStartupService extends ChannelStartupService {
if (!mediaMessage) { if (!mediaMessage) {
throw 'The message is not of the media type'; throw 'The message is not of the media type';
} }
}
if (typeof mediaMessage['mediaKey'] === 'object') { if (typeof mediaMessage['mediaKey'] === 'object') {
msg.message = JSON.parse(JSON.stringify(msg.message)); msg.message = JSON.parse(JSON.stringify(msg.message));
} }
const buffer = await downloadMediaMessage( let buffer: Buffer;
try {
buffer = await downloadMediaMessage(
{ key: msg?.key, message: msg?.message }, { key: msg?.key, message: msg?.message },
'buffer', 'buffer',
{}, {},
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, { logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage },
); );
} catch (err) {
this.logger.error('Download Media failed, trying to retry in 5 seconds...');
await new Promise((resolve) => setTimeout(resolve, 5000));
const mediaType = Object.keys(msg.message).find((key) => key.endsWith('Message'));
if (!mediaType) throw new Error('Could not determine mediaType for fallback');
try {
const media = await downloadContentFromMessage(
{
mediaKey: msg.message?.[mediaType]?.mediaKey,
directPath: msg.message?.[mediaType]?.directPath,
url: `https://mmg.whatsapp.net${msg?.message?.[mediaType]?.directPath}`,
},
await this.mapMediaType(mediaType),
{},
);
const chunks = [];
for await (const chunk of media) {
chunks.push(chunk);
}
buffer = Buffer.concat(chunks);
this.logger.info('Download Media with downloadContentFromMessage was successful!');
} catch (fallbackErr) {
this.logger.error('Download Media with downloadContentFromMessage also failed!');
throw fallbackErr;
}
}
const typeMessage = getContentType(msg.message); const typeMessage = getContentType(msg.message);
const ext = mimeTypes.extension(mediaMessage?.['mimetype']); const ext = mimeTypes.extension(mediaMessage?.['mimetype']);

View File

@ -26,7 +26,7 @@ import axios from 'axios';
import { proto } from 'baileys'; import { proto } from 'baileys';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import FormData from 'form-data'; import FormData from 'form-data';
import Jimp from 'jimp'; import { Jimp, JimpMime } from 'jimp';
import Long from 'long'; import Long from 'long';
import mimeTypes from 'mime-types'; import mimeTypes from 'mime-types';
import path from 'path'; import path from 'path';
@ -457,6 +457,24 @@ export class ChatwootService {
} }
} }
private async mergeContacts(baseId: number, mergeId: number) {
try {
const contact = await chatwootRequest(this.getClientCwConfig(), {
method: 'POST',
url: `/api/v1/accounts/${this.provider.accountId}/actions/contact_merge`,
body: {
base_contact_id: baseId,
mergee_contact_id: mergeId,
},
});
return contact;
} catch {
this.logger.error('Error merging contacts');
return null;
}
}
private async mergeBrazilianContacts(contacts: any[]) { private async mergeBrazilianContacts(contacts: any[]) {
try { try {
const contact = await chatwootRequest(this.getClientCwConfig(), { const contact = await chatwootRequest(this.getClientCwConfig(), {
@ -549,24 +567,37 @@ export class ChatwootService {
} }
public async createConversation(instance: InstanceDto, body: any) { public async createConversation(instance: InstanceDto, body: any) {
const isLid = body.key.remoteJid.includes('@lid') && body.key.senderPn; const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
const remoteJid = isLid ? body.key.senderPn : body.key.remoteJid; const remoteJid = body.key.remoteJid;
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`; const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`; const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
const maxWaitTime = 5000; // 5 secounds const maxWaitTime = 5000; // 5 secounds
try { try {
// Processa atualização de contatos já criados @lid // Processa atualização de contatos já criados @lid
if (body.key.remoteJid.includes('@lid') && body.key.senderPn && body.key.senderPn !== body.key.remoteJid) { if (
isLid &&
body.key.senderPn !== body.key.previousRemoteJid
) {
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]); const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
if (contact && contact.identifier !== body.key.senderPn) { if (contact && contact.identifier !== body.key.senderPn) {
this.logger.verbose( this.logger.verbose(
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn})`, `Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn}`,
); );
await this.updateContact(instance, contact.id, { const updateContact = await this.updateContact(instance, contact.id, {
identifier: body.key.senderPn, identifier: body.key.senderPn,
phone_number: `+${body.key.senderPn.split('@')[0]}`, phone_number: `+${body.key.senderPn.split('@')[0]}`,
}); });
if (updateContact === null) {
const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]);
if (baseContact) {
await this.mergeContacts(baseContact.id, contact.id);
this.logger.verbose(
`Merge contacts: (${baseContact.id}) ${baseContact.phone_number} and (${contact.id}) ${contact.phone_number}`,
);
}
}
} }
} }
this.logger.verbose(`--- Start createConversation ---`); this.logger.verbose(`--- Start createConversation ---`);
@ -685,7 +716,6 @@ export class ChatwootService {
} }
} }
} else { } else {
const jid = isLid && body?.key?.senderPn ? body.key.senderPn : body.key.remoteJid;
contact = await this.createContact( contact = await this.createContact(
instance, instance,
chatId, chatId,
@ -693,7 +723,7 @@ export class ChatwootService {
isGroup, isGroup,
nameContact, nameContact,
picture_url.profilePictureUrl || null, picture_url.profilePictureUrl || null,
jid, remoteJid,
); );
} }
@ -2101,9 +2131,11 @@ export class ChatwootService {
const fileData = Buffer.from(imgBuffer.data, 'binary'); const fileData = Buffer.from(imgBuffer.data, 'binary');
const img = await Jimp.read(fileData); const img = await Jimp.read(fileData);
await img.cover(320, 180); await img.cover({
w: 320,
const processedBuffer = await img.getBufferAsync(Jimp.MIME_PNG); h: 180,
});
const processedBuffer = await img.getBuffer(JimpMime.png);
const fileStream = new Readable(); const fileStream = new Readable();
fileStream._read = () => {}; // _read is required but you can noop it fileStream._read = () => {}; // _read is required but you can noop it

View File

@ -70,7 +70,7 @@ export class EvoaiService extends BaseChatbotService<Evoai, EvoaiSetting> {
} }
const callId = `req-${uuidv4().substring(0, 8)}`; const callId = `req-${uuidv4().substring(0, 8)}`;
const messageId = msg?.key?.id || uuidv4(); const messageId = remoteJid.split('@')[0] || uuidv4(); // Use phone number as messageId
// Prepare message parts // Prepare message parts
const parts = [ const parts = [

View File

@ -119,7 +119,7 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
const instanceData = await this.prismaRepository.instance.findFirst({ const instanceData = await this.prismaRepository.instance.findFirst({
where: { where: {
id: instance.instanceId, name: instance.instanceName,
}, },
}); });
@ -290,7 +290,7 @@ export class TypebotController extends BaseChatbotController<TypebotModel, Typeb
request.data.clientSideActions, request.data.clientSideActions,
); );
this.waMonitor.waInstances[instance.instanceId].sendDataWebhook(Events.TYPEBOT_START, { this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.TYPEBOT_START, {
remoteJid: remoteJid, remoteJid: remoteJid,
url: url, url: url,
typebot: typebot, typebot: typebot,

View File

@ -186,7 +186,7 @@ export class TypebotService extends BaseChatbotService<TypebotModel, any> {
messages, messages,
input, input,
clientSideActions, clientSideActions,
this.applyFormatting, this.applyFormatting.bind(this),
this.prismaRepository, this.prismaRepository,
).catch((err) => { ).catch((err) => {
console.error('Erro ao processar mensagens:', err); console.error('Erro ao processar mensagens:', err);

View File

@ -8,7 +8,12 @@ import { EmitData, EventController, EventControllerInterface } from '../event.co
export class RabbitmqController extends EventController implements EventControllerInterface { export class RabbitmqController extends EventController implements EventControllerInterface {
public amqpChannel: amqp.Channel | null = null; public amqpChannel: amqp.Channel | null = null;
private amqpConnection: amqp.Connection | null = null;
private readonly logger = new Logger('RabbitmqController'); private readonly logger = new Logger('RabbitmqController');
private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
private reconnectDelay = 5000; // 5 seconds
private isReconnecting = false;
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor, configService.get<Rabbitmq>('RABBITMQ')?.ENABLED, 'rabbitmq'); super(prismaRepository, waMonitor, configService.get<Rabbitmq>('RABBITMQ')?.ENABLED, 'rabbitmq');
@ -19,7 +24,11 @@ export class RabbitmqController extends EventController implements EventControll
return; return;
} }
await new Promise<void>((resolve, reject) => { await this.connect();
}
private async connect(): Promise<void> {
return new Promise<void>((resolve, reject) => {
const uri = configService.get<Rabbitmq>('RABBITMQ').URI; const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
const frameMax = configService.get<Rabbitmq>('RABBITMQ').FRAME_MAX; const frameMax = configService.get<Rabbitmq>('RABBITMQ').FRAME_MAX;
const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME; const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME;
@ -33,22 +42,61 @@ export class RabbitmqController extends EventController implements EventControll
password: url.password || 'guest', password: url.password || 'guest',
vhost: url.pathname.slice(1) || '/', vhost: url.pathname.slice(1) || '/',
frameMax: frameMax, frameMax: frameMax,
heartbeat: 30, // Add heartbeat of 30 seconds
}; };
amqp.connect(connectionOptions, (error, connection) => { amqp.connect(connectionOptions, (error, connection) => {
if (error) { if (error) {
this.logger.error({
local: 'RabbitmqController.connect',
message: 'Failed to connect to RabbitMQ',
error: error.message || error,
});
reject(error); reject(error);
return; return;
} }
// Connection event handlers
connection.on('error', (err) => {
this.logger.error({
local: 'RabbitmqController.connectionError',
message: 'RabbitMQ connection error',
error: err.message || err,
});
this.handleConnectionLoss();
});
connection.on('close', () => {
this.logger.warn('RabbitMQ connection closed');
this.handleConnectionLoss();
});
connection.createChannel((channelError, channel) => { connection.createChannel((channelError, channel) => {
if (channelError) { if (channelError) {
this.logger.error({
local: 'RabbitmqController.createChannel',
message: 'Failed to create RabbitMQ channel',
error: channelError.message || channelError,
});
reject(channelError); reject(channelError);
return; return;
} }
// Channel event handlers
channel.on('error', (err) => {
this.logger.error({
local: 'RabbitmqController.channelError',
message: 'RabbitMQ channel error',
error: err.message || err,
});
this.handleConnectionLoss();
});
channel.on('close', () => {
this.logger.warn('RabbitMQ channel closed');
this.handleConnectionLoss();
});
const exchangeName = rabbitmqExchangeName; const exchangeName = rabbitmqExchangeName;
channel.assertExchange(exchangeName, 'topic', { channel.assertExchange(exchangeName, 'topic', {
@ -56,16 +104,81 @@ export class RabbitmqController extends EventController implements EventControll
autoDelete: false, autoDelete: false,
}); });
this.amqpConnection = connection;
this.amqpChannel = channel; this.amqpChannel = channel;
this.reconnectAttempts = 0; // Reset reconnect attempts on successful connection
this.isReconnecting = false;
this.logger.info('AMQP initialized'); this.logger.info('AMQP initialized successfully');
resolve(); resolve();
}); });
}); });
}).then(() => { })
if (configService.get<Rabbitmq>('RABBITMQ')?.GLOBAL_ENABLED) this.initGlobalQueues(); .then(() => {
if (configService.get<Rabbitmq>('RABBITMQ')?.GLOBAL_ENABLED) {
this.initGlobalQueues();
}
})
.catch((error) => {
this.logger.error({
local: 'RabbitmqController.init',
message: 'Failed to initialize AMQP',
error: error.message || error,
}); });
this.scheduleReconnect();
throw error;
});
}
private handleConnectionLoss(): void {
if (this.isReconnecting) {
return; // Already attempting to reconnect
}
this.amqpChannel = null;
this.amqpConnection = null;
this.scheduleReconnect();
}
private scheduleReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.logger.error(
`Maximum reconnect attempts (${this.maxReconnectAttempts}) reached. Stopping reconnection attempts.`,
);
return;
}
if (this.isReconnecting) {
return; // Already scheduled
}
this.isReconnecting = true;
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, Math.min(this.reconnectAttempts - 1, 5)); // Exponential backoff with max delay
this.logger.info(
`Scheduling RabbitMQ reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`,
);
setTimeout(async () => {
try {
this.logger.info(
`Attempting to reconnect to RabbitMQ (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`,
);
await this.connect();
this.logger.info('Successfully reconnected to RabbitMQ');
} catch (error) {
this.logger.error({
local: 'RabbitmqController.scheduleReconnect',
message: `Reconnection attempt ${this.reconnectAttempts} failed`,
error: error.message || error,
});
this.isReconnecting = false;
this.scheduleReconnect();
}
}, delay);
} }
private set channel(channel: amqp.Channel) { private set channel(channel: amqp.Channel) {
@ -76,6 +189,17 @@ export class RabbitmqController extends EventController implements EventControll
return this.amqpChannel; return this.amqpChannel;
} }
private async ensureConnection(): Promise<boolean> {
if (!this.amqpChannel) {
this.logger.warn('AMQP channel is not available, attempting to reconnect...');
if (!this.isReconnecting) {
this.scheduleReconnect();
}
return false;
}
return true;
}
public async emit({ public async emit({
instanceName, instanceName,
origin, origin,
@ -95,6 +219,11 @@ export class RabbitmqController extends EventController implements EventControll
return; return;
} }
if (!(await this.ensureConnection())) {
this.logger.warn(`Failed to emit event ${event} for instance ${instanceName}: No AMQP connection`);
return;
}
const instanceRabbitmq = await this.get(instanceName); const instanceRabbitmq = await this.get(instanceName);
const rabbitmqLocal = instanceRabbitmq?.events; const rabbitmqLocal = instanceRabbitmq?.events;
const rabbitmqGlobal = configService.get<Rabbitmq>('RABBITMQ').GLOBAL_ENABLED; const rabbitmqGlobal = configService.get<Rabbitmq>('RABBITMQ').GLOBAL_ENABLED;
@ -154,7 +283,15 @@ export class RabbitmqController extends EventController implements EventControll
break; break;
} catch (error) { } catch (error) {
this.logger.error({
local: 'RabbitmqController.emit',
message: `Error publishing local RabbitMQ message (attempt ${retry + 1}/3)`,
error: error.message || error,
});
retry++; retry++;
if (retry >= 3) {
this.handleConnectionLoss();
}
} }
} }
} }
@ -199,7 +336,15 @@ export class RabbitmqController extends EventController implements EventControll
break; break;
} catch (error) { } catch (error) {
this.logger.error({
local: 'RabbitmqController.emit',
message: `Error publishing global RabbitMQ message (attempt ${retry + 1}/3)`,
error: error.message || error,
});
retry++; retry++;
if (retry >= 3) {
this.handleConnectionLoss();
}
} }
} }
} }
@ -208,33 +353,38 @@ export class RabbitmqController extends EventController implements EventControll
private async initGlobalQueues(): Promise<void> { private async initGlobalQueues(): Promise<void> {
this.logger.info('Initializing global queues'); this.logger.info('Initializing global queues');
if (!(await this.ensureConnection())) {
this.logger.error('Cannot initialize global queues: No AMQP connection');
return;
}
const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME; const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME;
const events = configService.get<Rabbitmq>('RABBITMQ').EVENTS; const events = configService.get<Rabbitmq>('RABBITMQ').EVENTS;
const prefixKey = configService.get<Rabbitmq>('RABBITMQ').PREFIX_KEY; const prefixKey = configService.get<Rabbitmq>('RABBITMQ').PREFIX_KEY;
if (!events) { if (!events) {
this.logger.warn('No events to initialize on AMQP'); this.logger.warn('No events to initialize on AMQP');
return; return;
} }
const eventKeys = Object.keys(events); const eventKeys = Object.keys(events);
eventKeys.forEach((event) => { for (const event of eventKeys) {
if (events[event] === false) return; if (events[event] === false) continue;
try {
const queueName = const queueName =
prefixKey !== '' prefixKey !== ''
? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}` ? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}`
: `${event.replace(/_/g, '.').toLowerCase()}`; : `${event.replace(/_/g, '.').toLowerCase()}`;
const exchangeName = rabbitmqExchangeName; const exchangeName = rabbitmqExchangeName;
this.amqpChannel.assertExchange(exchangeName, 'topic', { await this.amqpChannel.assertExchange(exchangeName, 'topic', {
durable: true, durable: true,
autoDelete: false, autoDelete: false,
}); });
this.amqpChannel.assertQueue(queueName, { await this.amqpChannel.assertQueue(queueName, {
durable: true, durable: true,
autoDelete: false, autoDelete: false,
arguments: { arguments: {
@ -242,7 +392,18 @@ export class RabbitmqController extends EventController implements EventControll
}, },
}); });
this.amqpChannel.bindQueue(queueName, exchangeName, event); await this.amqpChannel.bindQueue(queueName, exchangeName, event);
this.logger.info(`Global queue initialized: ${queueName}`);
} catch (error) {
this.logger.error({
local: 'RabbitmqController.initGlobalQueues',
message: `Failed to initialize global queue for event ${event}`,
error: error.message || error,
}); });
this.handleConnectionLoss();
break;
}
}
} }
} }

View File

@ -808,4 +808,28 @@ export class ChannelStartupService {
return []; return [];
} }
public hasValidMediaContent(message: any): boolean {
if (!message?.message) return false;
const msg = message.message;
// Se só tem messageContextInfo, não é mídia válida
if (Object.keys(msg).length === 1 && 'messageContextInfo' in msg) {
return false;
}
// Verifica se tem pelo menos um tipo de mídia válido
const mediaTypes = [
'imageMessage',
'videoMessage',
'stickerMessage',
'documentMessage',
'documentWithCaptionMessage',
'ptvMessage',
'audioMessage',
];
return mediaTypes.some((type) => msg[type] && Object.keys(msg[type]).length > 0);
}
} }

19
src/railway.json Normal file
View File

@ -0,0 +1,19 @@
{
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "Dockerfile"
},
"deploy": {
"runtime": "V2",
"numReplicas": 1,
"sleepApplication": false,
"multiRegionConfig": {
"us-east4-eqdc4a": {
"numReplicas": 1
}
},
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10
}
}