This commit is contained in:
Antônio Miguel 2025-07-11 18:16:29 +00:00 committed by GitHub
commit 713f6dde8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 2199 additions and 2001 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
@ -55,4 +57,4 @@ ENV DOCKER_ENV=true
EXPOSE 8080 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", "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

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

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,33 +1249,41 @@ 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;
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 mimetype = mimeTypes.lookup(fileName).toString(); const hasRealMedia = this.hasValidMediaContent(message);
const fullName = join(
`${this.instance.id}`,
received.key.remoteJid,
mediaType,
`${Date.now()}_${fileName}`,
);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
await this.prismaRepository.media.create({ if (!hasRealMedia) {
data: { this.logger.warn('Message detected as media but contains no valid media content');
messageId: msg.id, } else {
instanceId: this.instanceId, const media = await this.getBase64FromMediaMessage({ message }, true);
type: mediaType,
fileName: fullName,
mimetype,
},
});
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) { } 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,31 +2156,39 @@ 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;
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( const { buffer, mediaType, fileName, size } = media;
`${this.instance.id}`,
messageRaw.key.remoteJid,
`${messageRaw.key.id}`,
mediaType,
fileName,
);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype }); const mimetype = mimeTypes.lookup(fileName).toString();
await this.prismaRepository.media.create({ const fullName = join(
data: { messageId: msg.id, instanceId: this.instanceId, type: mediaType, fileName: fullName, mimetype }, `${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) { } 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;
@ -3453,12 +3508,39 @@ export class BaileysStartupService extends ChannelStartupService {
msg.message = JSON.parse(JSON.stringify(msg.message)); msg.message = JSON.parse(JSON.stringify(msg.message));
} }
const buffer = await downloadMediaMessage( let buffer: Buffer;
{ key: msg?.key, message: msg?.message },
'buffer', try {
{}, buffer = await downloadMediaMessage(
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage }, { 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 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

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