Compare commits

...

38 Commits

Author SHA1 Message Date
Willian Coqueiro
37319966db Remove redudantent code 2025-07-09 18:42:35 +00:00
Willian Coqueiro
630f5c5624 fix:
- [Baileys] Trocar @lids em remoteJid por senderPn em todos os serviços;
 - [Baileys] Adicionar valor @lid recebido em remoteJid para previousRemoteJid (Posteriormente utilizasse em ChatwootService);
 - Minors fixes;
2025-07-09 18:35:57 +00:00
KokeroO
52798fd761 fix(whatsapp-baileys): adjust indentation and access modifier for getBase64FromMediaMessage method 2025-06-27 09:29:12 -03:00
KokeroO
34446d188e fix(whatsapp-baileys):
- Implement broken event checking before duplicate message checking. (Do not process failed events).
- Implement error handling when downloading media with a fallback mechanism.
2025-06-27 09:03:32 -03:00
KokeroO
d7afe5d7ab fix(typebot): update instance query to use instance name instead of instance ID 2025-06-26 14:53:54 -03:00
KokeroO
e4f7856ca9 fix(typebot): update instance query to use instanceName instead of instanceId 2025-06-26 14:05:28 -03:00
Davidson Gomes
e86b6463fd Merge pull request #1652 from KokeroO/patch-1
Corrige ref. instance typebot.controller.ts
2025-06-26 11:49:41 -03:00
Willian Coqueiro
0302944654 Corrige ref. instance typebot.controller.ts 2025-06-26 11:42:27 -03:00
Davidson Gomes
1b0d81b022 Merge pull request #1641 from caduzin02/patch-1
Create railway.json
2025-06-25 16:32:30 -03:00
Davidson Gomes
854d357518 chore(package-lock): update Baileys dependency to version 6.7.18 2025-06-25 16:25:16 -03:00
caduzin02
6e8da4a8dc Create railway.json 2025-06-24 11:02:00 -03:00
Davidson Gomes
e92e98dd22 chore: optimize Dockerfile by using npm ci for dependency installation and streamlining file copy operations 2025-06-23 16:55:04 -03:00
Davidson Gomes
cdd2e59755 chore: update CHANGELOG for version 2.3.1 to include fix for S3 media upload 2025-06-23 16:51:54 -03:00
Davidson Gomes
1a1d9fc957 refactor: add validation for media content in Evolution and Business services to enhance error handling 2025-06-23 16:42:29 -03:00
Davidson Gomes
8ea4d65bc2 refactor: enhance media handling in Baileys service with validation for valid media content 2025-06-23 16:42:24 -03:00
Davidson Gomes
af713dee55 chore: update Dockerfile to use npm run build instead of npm run build:docker 2025-06-23 16:00:01 -03:00
Davidson Gomes
ee9ccb55ca chore: update version to 2.3.1 in Dockerfile, package.json, and package-lock.json 2025-06-23 15:41:49 -03:00
Davidson Gomes
977f686233 chore: update CHANGELOG for version 2.3.1 and fix package.json formatting 2025-06-23 15:38:54 -03:00
Davidson Gomes
a38caeaf5f chore(package-lock): update Baileys dependency to version 6.7.18 2025-06-23 15:36:54 -03:00
Davidson Gomes
873fdcb22a Merge pull request #1633 from VCalazans/FIX/ISSUE-28
🐛 fix: Phone number as message ID for Evo AI #ISSUE 28
2025-06-23 15:35:56 -03:00
Davidson Gomes
2e077a77ef Merge pull request #1626 from fernandeshenrique15/main
add unreadMessages in the response
2025-06-23 15:35:44 -03:00
Davidson Gomes
4be818a436 Merge pull request #1605 from ToniShelby/patch-2
Update Dockerfile
2025-06-23 15:35:33 -03:00
Davidson Gomes
a7badb9af5 Merge pull request #1624 from matheusfterra/main
Correção do envio de variáveis pelo typeboy
2025-06-23 15:35:20 -03:00
Davidson Gomes
ba5fb567eb Merge branch 'develop' into patch-2 2025-06-23 15:34:54 -03:00
Davidson Gomes
1070caf131 Merge pull request #1609 from KokeroO/feat/merge-contacts-lid-in-message-upsert
feat: Adiciona mesclagem de contatos @lid no Chatwoot
2025-06-23 15:32:46 -03:00
Davidson Gomes
9766e10ce9 Merge pull request #1623 from skarious/main
Update Dockerhub Repository and Delete Config Session Variable
2025-06-23 15:32:00 -03:00
Davidson Gomes
cc5612822f Merge pull request #1635 from autonomaia/patch-8
fix: corrige versão inválida do swagger-ui-express
2025-06-23 15:30:58 -03:00
autonomaia
83c1040114 fix: corrige versão inválida do swagger-ui-express 2025-06-22 22:10:51 -03:00
Victor Calazans
b4d1148d6a 🐛 fix: Phone number as message ID for Evo AI 2025-06-22 09:40:14 -03:00
Henrique Fernandes Neto
bd94423e5b add unreadMessages in the response 2025-06-19 16:30:57 -03:00
Matheus Terra
94670cbca3 Merge pull request #1 from matheusfterra/codex/corrigir-erro-ao-processar-mensagens-com-variáveis
Fix Typebot formatting function
2025-06-19 12:45:33 -03:00
Matheus Terra
bda3f5f146 Fix applyFormatting context in Typebot service 2025-06-19 12:45:09 -03:00
Ron Schneider
7724fe3a4f Merge branch 'main' into main 2025-06-19 02:11:50 -03:00
Ron Schneider
8b5f49592a Update evolution_api_v2.yaml
Update EvolutionApi DockerHub Repository and Delete Config Session Variable
2025-06-19 02:02:59 -03:00
Willian Coqueiro
6c0082cd7a feat: adicionar funcionalidade de mesclagem de contatos no Chatwoot 2025-06-17 22:33:51 +00:00
ToniShelby
3391b2ce4b Update Dockerfile 2025-06-17 16:33:50 +01:00
Ron Schneider
9db56560e4 Merge pull request #1 from skarious/skarious-patch-1
CONFIG_SESSION_PHONE_VERSION Updated
2025-05-29 14:06:12 -03:00
Ron Schneider
786e57603f CONFIG_SESSION_PHONE_VERSION Updated
CONFIG_SESSION_PHONE_VERSION updated
2025-05-29 14:05:42 -03:00
16 changed files with 2199 additions and 2001 deletions

View File

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

View File

@@ -196,7 +196,7 @@ CONFIG_SESSION_PHONE_NAME=Chrome
# Whatsapp Web version for baileys channel
# https://web.whatsapp.com/check-update?version=0&platform=web
# CONFIG_SESSION_PHONE_VERSION=2.3000.1023204200
# Set qrcode display limit
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)
### Feature

View File

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

View File

@@ -3,15 +3,17 @@ FROM node:20-alpine AS builder
RUN apk update && \
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 contact="contato@evolution-api.com"
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 ./public ./public
@@ -19,7 +21,6 @@ COPY ./prisma ./prisma
COPY ./manager ./manager
COPY ./.env.example ./.env
COPY ./runWithProvider.js ./
COPY ./tsup.config.ts ./
COPY ./Docker ./Docker
@@ -35,6 +36,7 @@ RUN apk update && \
apk add tzdata ffmpeg bash openssl
ENV TZ=America/Sao_Paulo
ENV DOCKER_ENV=true
WORKDIR /evolution
@@ -55,4 +57,4 @@ ENV DOCKER_ENV=true
EXPOSE 8080
ENTRYPOINT ["/bin/bash", "-c", ". ./Docker/scripts/deploy_database.sh && npm run start:prod" ]
ENTRYPOINT ["/bin/bash", "-c", ". ./Docker/scripts/deploy_database.sh && npm run start:prod" ]

3635
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -455,39 +455,46 @@ export class EvolutionStartupService extends ChannelStartupService {
if (base64 || file || audioFile) {
if (this.configService.get<S3>('S3').ENABLE) {
try {
const fileBuffer = audioFile?.buffer || file?.buffer;
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(messageRaw);
let mediaType: string;
let mimetype = audioFile?.mimetype || file.mimetype;
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const fileBuffer = audioFile?.buffer || file?.buffer;
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
if (messageRaw.messageType === 'documentMessage') {
mediaType = 'document';
mimetype = !mimetype ? 'application/pdf' : mimetype;
} else if (messageRaw.messageType === 'imageMessage') {
mediaType = 'image';
mimetype = !mimetype ? 'image/png' : mimetype;
} else if (messageRaw.messageType === 'audioMessage') {
mediaType = 'audio';
mimetype = !mimetype ? 'audio/mp4' : mimetype;
} else if (messageRaw.messageType === 'videoMessage') {
mediaType = 'video';
mimetype = !mimetype ? 'video/mp4' : mimetype;
let mediaType: string;
let mimetype = audioFile?.mimetype || file.mimetype;
if (messageRaw.messageType === 'documentMessage') {
mediaType = 'document';
mimetype = !mimetype ? 'application/pdf' : mimetype;
} else if (messageRaw.messageType === 'imageMessage') {
mediaType = 'image';
mimetype = !mimetype ? 'image/png' : mimetype;
} else if (messageRaw.messageType === 'audioMessage') {
mediaType = 'audio';
mimetype = !mimetype ? 'audio/mp4' : mimetype;
} else if (messageRaw.messageType === 'videoMessage') {
mediaType = 'video';
mimetype = !mimetype ? 'video/mp4' : mimetype;
}
const fileName = `${messageRaw.key.id}.${mimetype.split('/')[1]}`;
const size = buffer.byteLength;
const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName);
await s3Service.uploadFile(fullName, buffer, size, {
'Content-Type': mimetype,
});
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
}
const fileName = `${messageRaw.key.id}.${mimetype.split('/')[1]}`;
const size = buffer.byteLength;
const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName);
await s3Service.uploadFile(fullName, buffer, size, {
'Content-Type': mimetype,
});
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
} catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
}

View File

@@ -429,107 +429,114 @@ export class BusinessStartupService extends ChannelStartupService {
try {
const message: any = received;
const id = message.messages[0][message.messages[0].type].id;
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
urlServer = `${urlServer}/${version}/${id}`;
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
const result = await axios.get(urlServer, { headers });
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(messageRaw);
const buffer = await axios.get(result.data.url, {
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
responseType: 'arraybuffer',
});
let mediaType;
if (message.messages[0].document) {
mediaType = 'document';
} else if (message.messages[0].image) {
mediaType = 'image';
} else if (message.messages[0].audio) {
mediaType = 'audio';
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
mediaType = 'video';
}
const id = message.messages[0][message.messages[0].type].id;
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
urlServer = `${urlServer}/${version}/${id}`;
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
const result = await axios.get(urlServer, { headers });
const mimetype = result.data?.mime_type || result.headers['content-type'];
const buffer = await axios.get(result.data.url, {
headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download
responseType: 'arraybuffer',
});
const contentDisposition = result.headers['content-disposition'];
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
if (contentDisposition) {
const match = contentDisposition.match(/filename="(.+?)"/);
if (match) {
fileName = match[1];
let mediaType;
if (message.messages[0].document) {
mediaType = 'document';
} else if (message.messages[0].image) {
mediaType = 'image';
} else if (message.messages[0].audio) {
mediaType = 'audio';
} else {
mediaType = 'video';
}
}
// Para áudio, garantir extensão correta baseada no mimetype
if (mediaType === 'audio') {
if (mimetype.includes('ogg')) {
fileName = `${message.messages[0].id}.ogg`;
} else if (mimetype.includes('mp3')) {
fileName = `${message.messages[0].id}.mp3`;
} else if (mimetype.includes('m4a')) {
fileName = `${message.messages[0].id}.m4a`;
const mimetype = result.data?.mime_type || result.headers['content-type'];
const contentDisposition = result.headers['content-disposition'];
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
if (contentDisposition) {
const match = contentDisposition.match(/filename="(.+?)"/);
if (match) {
fileName = match[1];
}
}
}
const size = result.headers['content-length'] || buffer.data.byteLength;
// Para áudio, garantir extensão correta baseada no mimetype
if (mediaType === 'audio') {
if (mimetype.includes('ogg')) {
fileName = `${message.messages[0].id}.ogg`;
} else if (mimetype.includes('mp3')) {
fileName = `${message.messages[0].id}.mp3`;
} else if (mimetype.includes('m4a')) {
fileName = `${message.messages[0].id}.m4a`;
}
}
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
const size = result.headers['content-length'] || buffer.data.byteLength;
await s3Service.uploadFile(fullName, buffer.data, size, {
'Content-Type': mimetype,
});
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
const createdMessage = await this.prismaRepository.message.create({
data: messageRaw,
});
await s3Service.uploadFile(fullName, buffer.data, size, {
'Content-Type': mimetype,
});
await this.prismaRepository.media.create({
data: {
messageId: createdMessage.id,
instanceId: this.instanceId,
type: mediaType,
fileName: fullName,
mimetype,
},
});
const createdMessage = await this.prismaRepository.message.create({
data: messageRaw,
});
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
messageRaw.message.base64 = buffer.data.toString('base64');
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: {
await this.prismaRepository.media.create({
data: {
messageId: createdMessage.id,
instanceId: this.instanceId,
},
include: {
OpenaiCreds: true,
type: mediaType,
fileName: fullName,
mimetype,
},
});
if (
openAiDefaultSettings &&
openAiDefaultSettings.openaiCredsId &&
openAiDefaultSettings.speechToText
) {
try {
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
{
message: {
mediaUrl: messageRaw.message.mediaUrl,
...messageRaw,
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
messageRaw.message.base64 = buffer.data.toString('base64');
// Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível
if (this.configService.get<Openai>('OPENAI').ENABLED && mediaType === 'audio') {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: {
instanceId: this.instanceId,
},
include: {
OpenaiCreds: true,
},
});
if (
openAiDefaultSettings &&
openAiDefaultSettings.openaiCredsId &&
openAiDefaultSettings.speechToText
) {
try {
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
{
message: {
mediaUrl: messageRaw.message.mediaUrl,
...messageRaw,
},
},
},
)}`;
} catch (speechError) {
this.logger.error(`Error processing speech-to-text: ${speechError}`);
)}`;
} catch (speechError) {
this.logger.error(`Error processing speech-to-text: ${speechError}`);
}
}
}
}

View File

@@ -99,6 +99,7 @@ import makeWASocket, {
Contact,
delay,
DisconnectReason,
downloadContentFromMessage,
downloadMediaMessage,
generateWAMessageFromContent,
getAggregateVotesInPollMessage,
@@ -122,7 +123,7 @@ import makeWASocket, {
WABrowserDescription,
WAMediaUpload,
WAMessage,
WAMessageUpdate,
WAMessageKey,
WAPresence,
WASocket,
} from 'baileys';
@@ -887,7 +888,7 @@ export class BaileysStartupService extends ChannelStartupService {
}: {
chats: Chat[];
contacts: Contact[];
messages: proto.IWebMessageInfo[];
messages: WAMessage[];
isLatest?: boolean;
progress?: number;
syncType?: proto.HistorySync.HistorySyncType;
@@ -973,6 +974,10 @@ export class BaileysStartupService extends ChannelStartupService {
continue;
}
if (m.key.remoteJid?.includes('@lid') && m.key.senderPn) {
m.key.remoteJid = m.key.senderPn;
}
if (Long.isLong(m?.messageTimestamp)) {
m.messageTimestamp = m.messageTimestamp?.toNumber();
}
@@ -1030,11 +1035,29 @@ export class BaileysStartupService extends ChannelStartupService {
},
'messages.upsert': async (
{ messages, type, requestId }: { messages: proto.IWebMessageInfo[]; type: MessageUpsertType; requestId?: string },
{ messages, type, requestId }: { messages: WAMessage[]; type: MessageUpsertType; requestId?: string },
settings: any,
) => {
try {
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) {
const text = received.message?.conversation || received.message?.extendedTextMessage?.text;
@@ -1226,33 +1249,41 @@ export class BaileysStartupService extends ChannelStartupService {
if (this.configService.get<S3>('S3').ENABLE) {
try {
const message: any = received;
const media = await this.getBase64FromMediaMessage({ message }, true);
const { buffer, mediaType, fileName, size } = media;
const mimetype = mimeTypes.lookup(fileName).toString();
const fullName = join(
`${this.instance.id}`,
received.key.remoteJid,
mediaType,
`${Date.now()}_${fileName}`,
);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(message);
await this.prismaRepository.media.create({
data: {
messageId: msg.id,
instanceId: this.instanceId,
type: mediaType,
fileName: fullName,
mimetype,
},
});
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const media = await this.getBase64FromMediaMessage({ message }, true);
const mediaUrl = await s3Service.getObjectUrl(fullName);
const { buffer, mediaType, fileName, size } = media;
const mimetype = mimeTypes.lookup(fileName).toString();
const fullName = join(
`${this.instance.id}`,
received.key.remoteJid,
mediaType,
`${Date.now()}_${fileName}`,
);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
messageRaw.message.mediaUrl = mediaUrl;
await this.prismaRepository.media.create({
data: {
messageId: msg.id,
instanceId: this.instanceId,
type: mediaType,
fileName: fullName,
mimetype,
},
});
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
}
} catch (error) {
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)}`);
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
@@ -1366,6 +1397,10 @@ export class BaileysStartupService extends ChannelStartupService {
continue;
}
if (key.remoteJid?.includes('@lid') && key.senderPn) {
key.remoteJid = key.senderPn;
}
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
const cached = await this.baileysCache.get(updateKey);
@@ -2121,31 +2156,39 @@ export class BaileysStartupService extends ChannelStartupService {
if (isMedia && this.configService.get<S3>('S3').ENABLE) {
try {
const message: any = messageRaw;
const media = await this.getBase64FromMediaMessage({ message }, true);
const { buffer, mediaType, fileName, size } = media;
// Verificação adicional para garantir que há conteúdo de mídia real
const hasRealMedia = this.hasValidMediaContent(message);
const mimetype = mimeTypes.lookup(fileName).toString();
if (!hasRealMedia) {
this.logger.warn('Message detected as media but contains no valid media content');
} else {
const media = await this.getBase64FromMediaMessage({ message }, true);
const fullName = join(
`${this.instance.id}`,
messageRaw.key.remoteJid,
`${messageRaw.key.id}`,
mediaType,
fileName,
);
const { buffer, mediaType, fileName, size } = media;
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
const mimetype = mimeTypes.lookup(fileName).toString();
await this.prismaRepository.media.create({
data: { messageId: msg.id, instanceId: this.instanceId, type: mediaType, fileName: fullName, mimetype },
});
const fullName = join(
`${this.instance.id}`,
messageRaw.key.remoteJid,
`${messageRaw.key.id}`,
mediaType,
fileName,
);
const mediaUrl = await s3Service.getObjectUrl(fullName);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
messageRaw.message.mediaUrl = mediaUrl;
await this.prismaRepository.media.create({
data: { messageId: msg.id, instanceId: this.instanceId, type: mediaType, fileName: fullName, mimetype },
});
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
}
} catch (error) {
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) {
try {
const m = data?.message;
@@ -3453,12 +3508,39 @@ export class BaileysStartupService extends ChannelStartupService {
msg.message = JSON.parse(JSON.stringify(msg.message));
}
const buffer = await downloadMediaMessage(
{ key: msg?.key, message: msg?.message },
'buffer',
{},
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage },
);
let buffer: Buffer;
try {
buffer = await downloadMediaMessage(
{ key: msg?.key, message: msg?.message },
'buffer',
{},
{ 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'));
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!');
}
}
const typeMessage = getContentType(msg.message);
const ext = mimeTypes.extension(mediaMessage?.['mimetype']);

View File

@@ -26,7 +26,7 @@ import axios from 'axios';
import { proto } from 'baileys';
import dayjs from 'dayjs';
import FormData from 'form-data';
import Jimp from 'jimp';
import { Jimp, JimpMime } from 'jimp';
import Long from 'long';
import mimeTypes from 'mime-types';
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[]) {
try {
const contact = await chatwootRequest(this.getClientCwConfig(), {
@@ -549,24 +567,37 @@ export class ChatwootService {
}
public async createConversation(instance: InstanceDto, body: any) {
const isLid = body.key.remoteJid.includes('@lid') && body.key.senderPn;
const remoteJid = isLid ? body.key.senderPn : body.key.remoteJid;
const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn;
const remoteJid = body.key.remoteJid;
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
const maxWaitTime = 5000; // 5 secounds
try {
// 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]);
if (contact && contact.identifier !== body.key.senderPn) {
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,
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 ---`);
@@ -685,7 +716,6 @@ export class ChatwootService {
}
}
} else {
const jid = isLid && body?.key?.senderPn ? body.key.senderPn : body.key.remoteJid;
contact = await this.createContact(
instance,
chatId,
@@ -693,7 +723,7 @@ export class ChatwootService {
isGroup,
nameContact,
picture_url.profilePictureUrl || null,
jid,
remoteJid,
);
}
@@ -2101,9 +2131,11 @@ export class ChatwootService {
const fileData = Buffer.from(imgBuffer.data, 'binary');
const img = await Jimp.read(fileData);
await img.cover(320, 180);
const processedBuffer = await img.getBufferAsync(Jimp.MIME_PNG);
await img.cover({
w: 320,
h: 180,
});
const processedBuffer = await img.getBuffer(JimpMime.png);
const fileStream = new Readable();
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 messageId = msg?.key?.id || uuidv4();
const messageId = remoteJid.split('@')[0] || uuidv4(); // Use phone number as messageId
// Prepare message parts
const parts = [

View File

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

View File

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

View File

@@ -738,6 +738,7 @@ export class ChannelStartupService {
"Chat"."name" as "pushName",
"Chat"."createdAt" as "windowStart",
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
"Chat"."unreadMessages" as "unreadMessages",
CASE WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true ELSE false END as "windowActive",
"Message"."id" AS lastMessageId,
"Message"."key" AS lastMessage_key,
@@ -797,7 +798,7 @@ export class ChannelStartupService {
windowExpires: contact.windowexpires,
windowActive: contact.windowactive,
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
unreadCount: 0,
unreadCount: contact.unreadMessages,
isSaved: !!contact.contactid,
};
});
@@ -813,4 +814,28 @@ export class ChannelStartupService {
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
}
}