Correções.

This commit is contained in:
Éder Costa 2025-05-04 10:25:32 -03:00
parent 9c57866b3f
commit fcf6b03b4c
7 changed files with 195 additions and 60 deletions

View File

@ -3,7 +3,7 @@ FROM node:20-alpine AS builder
RUN apk update && \ RUN apk update && \
apk add git ffmpeg wget curl bash openssl apk add git ffmpeg wget curl bash openssl
LABEL version="2.2.3.17" description="Api to control whatsapp features through http requests." LABEL version="2.2.3.22" 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@atendai.com" LABEL contact="contato@atendai.com"

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "evolution-api", "name": "evolution-api",
"version": "2.2.3.17", "version": "2.2.3.22",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "evolution-api", "name": "evolution-api",
"version": "2.2.3.17", "version": "2.2.3.22",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@adiwajshing/keyed-db": "^0.2.4", "@adiwajshing/keyed-db": "^0.2.4",

View File

@ -1,6 +1,6 @@
{ {
"name": "evolution-api", "name": "evolution-api",
"version": "2.2.3.17", "version": "2.2.3.22",
"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",

View File

@ -19,11 +19,43 @@ import { Query } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service'; import { WAMonitoringService } from '@api/services/monitor.service';
import { Contact, Message, MessageUpdate } from '@prisma/client'; import { Contact, Message, MessageUpdate } from '@prisma/client';
class SimpleMutex {
private locked = false;
private waiting: Array<() => void> = [];
async acquire(): Promise<void> {
if (this.locked) {
await new Promise<void>(resolve => this.waiting.push(resolve));
}
this.locked = true;
}
release(): void {
const next = this.waiting.shift();
if (next) next();
else this.locked = false;
}
async runExclusive<T>(fn: () => Promise<T>): Promise<T> {
await this.acquire();
try {
return await fn();
} finally {
this.release();
}
}
}
export class ChatController { export class ChatController {
constructor(private readonly waMonitor: WAMonitoringService) {} constructor(private readonly waMonitor: WAMonitoringService) {}
private static whatsappNumberMutex = new SimpleMutex();
public async whatsappNumber({ instanceName }: InstanceDto, data: WhatsAppNumberDto) { public async whatsappNumber({ instanceName }: InstanceDto, data: WhatsAppNumberDto) {
return await this.waMonitor.waInstances[instanceName].whatsappNumber(data); return await ChatController.whatsappNumberMutex.runExclusive(async () => {
return this.waMonitor.waInstances[instanceName].whatsappNumber(data);
});
} }
public async readMessage({ instanceName }: InstanceDto, data: ReadMessageDto) { public async readMessage({ instanceName }: InstanceDto, data: ReadMessageDto) {

View File

@ -136,7 +136,7 @@ import mimeTypes from 'mime-types';
import NodeCache from 'node-cache'; import NodeCache from 'node-cache';
import cron from 'node-cron'; import cron from 'node-cron';
import { release } from 'os'; import { release } from 'os';
import { join } from 'path'; import path, { join } from 'path';
import P from 'pino'; import P from 'pino';
import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; import qrcode, { QRCodeToDataURLOptions } from 'qrcode';
import qrcodeTerminal from 'qrcode-terminal'; import qrcodeTerminal from 'qrcode-terminal';
@ -1296,13 +1296,25 @@ export class BaileysStartupService extends ChannelStartupService {
true, true,
); );
const { buffer, mediaType, fileName, size } = media; const { buffer, mediaType, fileName: originalName, size } = media;
const mimetype = mimeTypes.lookup(fileName).toString(); const mimetype = mimeTypes.lookup(originalName).toString();
const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName);
// calcula a extensão (usa a do nome original ou, em último caso, a do mimetype)
const ext = path.extname(originalName) || `.${mimeTypes.extension(mimetype)}`;
// força usar sempre o id da mensagem como nome de arquivo
const fileName = `${received.key.id}${ext}`;
const fullName = join(
this.instance.id,
received.key.remoteJid,
mediaType,
fileName,
);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, {
'Content-Type': mimetype, 'Content-Type': mimetype,
}); });
await this.prismaRepository.media.create({ await this.prismaRepository.media.create({
data: { data: {
messageId: msg.id, messageId: msg.id,
@ -1428,6 +1440,11 @@ export class BaileysStartupService extends ChannelStartupService {
continue; continue;
} }
if (!key.id) {
console.warn(`Mensagem sem key.id, pulando update: ${JSON.stringify(key)}`);
continue;
}
if (status[update.status] === 'READ' && key.fromMe) { if (status[update.status] === 'READ' && key.fromMe) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) { if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
this.chatwootService.eventWhatsapp( this.chatwootService.eventWhatsapp(
@ -1515,7 +1532,7 @@ export class BaileysStartupService extends ChannelStartupService {
remoteJid: key.remoteJid, remoteJid: key.remoteJid,
fromMe: key.fromMe, fromMe: key.fromMe,
participant: key?.remoteJid, participant: key?.remoteJid,
status: status[update.status], status: status[update.status]?? 'UNKNOWN',
pollUpdates, pollUpdates,
instanceId: this.instanceId, instanceId: this.instanceId,
}; };
@ -4476,29 +4493,41 @@ export class BaileysStartupService extends ChannelStartupService {
return unreadMessages; return unreadMessages;
} }
private async addLabel(labelId: string, instanceId: string, chatId: string) { private async addLabel(
const id = cuid(); labelId: string,
instanceId: string,
await this.prismaRepository.$executeRawUnsafe( chatId: string
`INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt") ): Promise<void> {
VALUES ($4, $2, $3, to_jsonb(ARRAY[$1]::text[]), NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid") try {
DO await this.prismaRepository.$executeRawUnsafe(
UPDATE `UPDATE "Chat"
SET "labels" = ( SET "labels" = (
SELECT to_jsonb(array_agg(DISTINCT elem)) SELECT to_jsonb(array_agg(DISTINCT elem))
FROM ( FROM (
SELECT jsonb_array_elements_text("Chat"."labels") AS elem SELECT jsonb_array_elements_text("Chat".labels) AS elem
UNION UNION
SELECT $1::text AS elem SELECT $1::text AS elem
) sub ) sub
), ),
"updatedAt" = NOW();`, "updatedAt" = NOW()
labelId, WHERE "instanceId" = $2
instanceId, AND "remoteJid" = $3;`,
chatId, labelId,
id, instanceId,
); chatId
);
} catch (err: unknown) {
// Não deixa quebrar nada: registra e segue em frente
const msg =
err instanceof Error ? err.message : JSON.stringify(err);
// Use console.warn para evitar conflito de assinatura de método
console.warn(
`Failed to add label ${labelId} to chat ${chatId}@${instanceId}: ${msg}`
);
}
} }
private async removeLabel(labelId: string, instanceId: string, chatId: string) { private async removeLabel(labelId: string, instanceId: string, chatId: string) {
const id = cuid(); const id = cuid();

View File

@ -2171,27 +2171,47 @@ export class ChatwootService {
} }
if (body.key.remoteJid.includes('@g.us')) { if (body.key.remoteJid.includes('@g.us')) {
const participantName = body.pushName; // Extrai de forma segura o JID do participante
const rawPhoneNumber = body.key.participant.split('@')[0]; const participantJid = body.key.participant;
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
// Se não veio participant, envia mensagem crua
let formattedPhoneNumber: string; if (!participantJid) {
const rawContent = bodyMessage;
if (phoneMatch) { const sent = await this.createMessage(
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`; instance,
} else { getConversation,
formattedPhoneNumber = `+${rawPhoneNumber}`; rawContent,
messageType,
false,
[],
body,
'WAID:' + body.key.id,
quotedMsg,
);
if (!sent) this.logger.warn('message not sent');
return sent;
} }
let content: string; // Formata o telefone
const rawPhone = participantJid.split('@')[0];
if (!body.key.fromMe) { const match = rawPhone.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`; const formattedPhone = match
} else { ? `+${match[1]} (${match[2]}) ${match[3]}-${match[4]}`
content = `${bodyMessage}`; : `+${rawPhone}`;
}
// Define prefixo com número e nome (ou só número, se pushName vazio)
const send = await this.createMessage( const name = body.pushName?.trim();
const prefix = name
? `**${formattedPhone} ${name}:**\n\n`
: `**${formattedPhone}:**\n\n`;
// Monta o conteúdo, omitindo prefixo em mensagens enviadas por mim
const content = body.key.fromMe
? bodyMessage
: `${prefix}${bodyMessage}`;
// Envia a mensagem formatada
const sent = await this.createMessage(
instance, instance,
getConversation, getConversation,
content, content,
@ -2202,13 +2222,8 @@ export class ChatwootService {
'WAID:' + body.key.id, 'WAID:' + body.key.id,
quotedMsg, quotedMsg,
); );
if (!sent) this.logger.warn('message not sent');
if (!send) { return sent;
this.logger.warn('message not sent');
return;
}
return send;
} else { } else {
const send = await this.createMessage( const send = await this.createMessage(
instance, instance,

View File

@ -6,6 +6,7 @@ import { Chatwoot, configService } from '@config/env.config';
import { Logger } from '@config/logger.config'; import { Logger } from '@config/logger.config';
import { inbox } from '@figuro/chatwoot-sdk'; import { inbox } from '@figuro/chatwoot-sdk';
import { Chatwoot as ChatwootModel, Contact, Message } from '@prisma/client'; import { Chatwoot as ChatwootModel, Contact, Message } from '@prisma/client';
import axios from 'axios';
import { proto } from 'baileys'; import { proto } from 'baileys';
type ChatwootUser = { type ChatwootUser = {
@ -209,6 +210,7 @@ class ChatwootImport {
throw new Error('User not found to import messages.'); throw new Error('User not found to import messages.');
} }
const touchedConversations = new Set<string>();
let totalMessagesImported = 0; let totalMessagesImported = 0;
let messagesOrdered = this.historyMessages.get(instance.instanceName) || []; let messagesOrdered = this.historyMessages.get(instance.instanceName) || [];
@ -284,6 +286,11 @@ class ChatwootImport {
phoneNumbersWithTimestamp, phoneNumbersWithTimestamp,
messagesByPhoneNumber, messagesByPhoneNumber,
); );
for (const { conversation_id } of fksByNumber.values()) {
touchedConversations.add(conversation_id);
}
this.logger.info( this.logger.info(
`[importHistoryMessages] Batch ${batchNumber}: FKs recuperados para ${fksByNumber.size} números.` `[importHistoryMessages] Batch ${batchNumber}: FKs recuperados para ${fksByNumber.size} números.`
); );
@ -336,6 +343,8 @@ class ChatwootImport {
${bindSenderType},${bindSenderId},${bindSourceId}, to_timestamp(${bindmessageTimestamp}), to_timestamp(${bindmessageTimestamp})),`; ${bindSenderType},${bindSenderId},${bindSourceId}, to_timestamp(${bindmessageTimestamp}), to_timestamp(${bindmessageTimestamp})),`;
}); });
}); });
if (bindInsertMsg.length > 2) { if (bindInsertMsg.length > 2) {
if (sqlInsertMsg.slice(-1) === ',') { if (sqlInsertMsg.slice(-1) === ',') {
sqlInsertMsg = sqlInsertMsg.slice(0, -1); sqlInsertMsg = sqlInsertMsg.slice(0, -1);
@ -354,10 +363,24 @@ class ChatwootImport {
this.deleteHistoryMessages(instance); this.deleteHistoryMessages(instance);
this.deleteRepositoryMessagesCache(instance); this.deleteRepositoryMessagesCache(instance);
this.logger.info( this.logger.info(
`[importHistoryMessages] Histórico e cache de mensagens da instância "${instance.instanceName}" foram limpos.` `[importHistoryMessages] Histórico e cache de mensagens da instância "${instance.instanceName}" foram limpos.`
); );
for (const convId of touchedConversations) {
await this.safeRefreshConversation(
provider.url,
provider.accountId,
convId,
provider.token
);
}
const providerData: ChatwootDto = { const providerData: ChatwootDto = {
...provider, ...provider,
ignoreJids: Array.isArray(provider.ignoreJids) ? provider.ignoreJids.map((event) => String(event)) : [], ignoreJids: Array.isArray(provider.ignoreJids) ? provider.ignoreJids.map((event) => String(event)) : [],
@ -719,6 +742,42 @@ class ChatwootImport {
return pgClient.query(sql, [`WAID:${sourceId}`, messageId]); return pgClient.query(sql, [`WAID:${sourceId}`, messageId]);
} }
private async safeRefreshConversation(
providerUrl: string,
accountId: string,
conversationId: string,
apiToken: string
): Promise<void> {
try {
const pgClient = postgresClient.getChatwootConnection();
const res = await pgClient.query(
`SELECT display_id
FROM conversations
WHERE id = $1
LIMIT 1`,
[parseInt(conversationId, 10)]
);
const displayId = res.rows[0]?.display_id as string;
if (!displayId) {
this.logger.warn(`Conversation ${conversationId} sem display_id.`);
return;
}
const url = `${providerUrl}/api/v1/accounts/${accountId}/conversations/${displayId}/refresh`;
await axios.post(url, null, {
params: { api_access_token: apiToken },
});
this.logger.verbose(`Conversa ${displayId} refreshada com sucesso.`);
} catch (err: any) {
this.logger.warn(
`Não foi possível dar refresh na conversa ${conversationId}: ${err.message}`
);
}
}
} }
export const chatwootImport = new ChatwootImport(); export const chatwootImport = new ChatwootImport();