mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-21 19:47:21 -06:00
Correções.
This commit is contained in:
parent
9c57866b3f
commit
fcf6b03b4c
@ -3,7 +3,7 @@ FROM node:20-alpine AS builder
|
||||
RUN apk update && \
|
||||
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 contact="contato@atendai.com"
|
||||
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "evolution-api",
|
||||
"version": "2.2.3.17",
|
||||
"version": "2.2.3.22",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "evolution-api",
|
||||
"version": "2.2.3.17",
|
||||
"version": "2.2.3.22",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "evolution-api",
|
||||
"version": "2.2.3.17",
|
||||
"version": "2.2.3.22",
|
||||
"description": "Rest api for communication with WhatsApp",
|
||||
"main": "./dist/main.js",
|
||||
"type": "commonjs",
|
||||
|
@ -19,11 +19,43 @@ import { Query } from '@api/repository/repository.service';
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
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 {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
private static whatsappNumberMutex = new SimpleMutex();
|
||||
|
||||
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) {
|
||||
|
@ -136,7 +136,7 @@ import mimeTypes from 'mime-types';
|
||||
import NodeCache from 'node-cache';
|
||||
import cron from 'node-cron';
|
||||
import { release } from 'os';
|
||||
import { join } from 'path';
|
||||
import path, { join } from 'path';
|
||||
import P from 'pino';
|
||||
import qrcode, { QRCodeToDataURLOptions } from 'qrcode';
|
||||
import qrcodeTerminal from 'qrcode-terminal';
|
||||
@ -1296,13 +1296,25 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
true,
|
||||
);
|
||||
|
||||
const { buffer, mediaType, fileName, size } = media;
|
||||
const mimetype = mimeTypes.lookup(fileName).toString();
|
||||
const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName);
|
||||
const { buffer, mediaType, fileName: originalName, size } = media;
|
||||
const mimetype = mimeTypes.lookup(originalName).toString();
|
||||
|
||||
// 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, {
|
||||
'Content-Type': mimetype,
|
||||
});
|
||||
|
||||
await this.prismaRepository.media.create({
|
||||
data: {
|
||||
messageId: msg.id,
|
||||
@ -1428,6 +1440,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!key.id) {
|
||||
console.warn(`Mensagem sem key.id, pulando update: ${JSON.stringify(key)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status[update.status] === 'READ' && key.fromMe) {
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||
this.chatwootService.eventWhatsapp(
|
||||
@ -1515,7 +1532,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
remoteJid: key.remoteJid,
|
||||
fromMe: key.fromMe,
|
||||
participant: key?.remoteJid,
|
||||
status: status[update.status],
|
||||
status: status[update.status]?? 'UNKNOWN',
|
||||
pollUpdates,
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
@ -4476,29 +4493,41 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
return unreadMessages;
|
||||
}
|
||||
|
||||
private async addLabel(labelId: string, instanceId: string, chatId: string) {
|
||||
const id = cuid();
|
||||
|
||||
await this.prismaRepository.$executeRawUnsafe(
|
||||
`INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt")
|
||||
VALUES ($4, $2, $3, to_jsonb(ARRAY[$1]::text[]), NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid")
|
||||
DO
|
||||
UPDATE
|
||||
SET "labels" = (
|
||||
SELECT to_jsonb(array_agg(DISTINCT elem))
|
||||
FROM (
|
||||
SELECT jsonb_array_elements_text("Chat"."labels") AS elem
|
||||
UNION
|
||||
SELECT $1::text AS elem
|
||||
) sub
|
||||
),
|
||||
"updatedAt" = NOW();`,
|
||||
labelId,
|
||||
instanceId,
|
||||
chatId,
|
||||
id,
|
||||
);
|
||||
private async addLabel(
|
||||
labelId: string,
|
||||
instanceId: string,
|
||||
chatId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.prismaRepository.$executeRawUnsafe(
|
||||
`UPDATE "Chat"
|
||||
SET "labels" = (
|
||||
SELECT to_jsonb(array_agg(DISTINCT elem))
|
||||
FROM (
|
||||
SELECT jsonb_array_elements_text("Chat".labels) AS elem
|
||||
UNION
|
||||
SELECT $1::text AS elem
|
||||
) sub
|
||||
),
|
||||
"updatedAt" = NOW()
|
||||
WHERE "instanceId" = $2
|
||||
AND "remoteJid" = $3;`,
|
||||
labelId,
|
||||
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) {
|
||||
const id = cuid();
|
||||
|
@ -2171,27 +2171,47 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
if (body.key.remoteJid.includes('@g.us')) {
|
||||
const participantName = body.pushName;
|
||||
const rawPhoneNumber = body.key.participant.split('@')[0];
|
||||
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
||||
|
||||
let formattedPhoneNumber: string;
|
||||
|
||||
if (phoneMatch) {
|
||||
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
|
||||
} else {
|
||||
formattedPhoneNumber = `+${rawPhoneNumber}`;
|
||||
// Extrai de forma segura o JID do participante
|
||||
const participantJid = body.key.participant;
|
||||
|
||||
// Se não veio participant, envia mensagem crua
|
||||
if (!participantJid) {
|
||||
const rawContent = bodyMessage;
|
||||
const sent = await this.createMessage(
|
||||
instance,
|
||||
getConversation,
|
||||
rawContent,
|
||||
messageType,
|
||||
false,
|
||||
[],
|
||||
body,
|
||||
'WAID:' + body.key.id,
|
||||
quotedMsg,
|
||||
);
|
||||
if (!sent) this.logger.warn('message not sent');
|
||||
return sent;
|
||||
}
|
||||
|
||||
let content: string;
|
||||
|
||||
if (!body.key.fromMe) {
|
||||
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
|
||||
} else {
|
||||
content = `${bodyMessage}`;
|
||||
}
|
||||
|
||||
const send = await this.createMessage(
|
||||
|
||||
// Formata o telefone
|
||||
const rawPhone = participantJid.split('@')[0];
|
||||
const match = rawPhone.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
||||
const formattedPhone = match
|
||||
? `+${match[1]} (${match[2]}) ${match[3]}-${match[4]}`
|
||||
: `+${rawPhone}`;
|
||||
|
||||
// Define prefixo com número e nome (ou só número, se pushName vazio)
|
||||
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,
|
||||
getConversation,
|
||||
content,
|
||||
@ -2202,13 +2222,8 @@ export class ChatwootService {
|
||||
'WAID:' + body.key.id,
|
||||
quotedMsg,
|
||||
);
|
||||
|
||||
if (!send) {
|
||||
this.logger.warn('message not sent');
|
||||
return;
|
||||
}
|
||||
|
||||
return send;
|
||||
if (!sent) this.logger.warn('message not sent');
|
||||
return sent;
|
||||
} else {
|
||||
const send = await this.createMessage(
|
||||
instance,
|
||||
|
@ -6,6 +6,7 @@ import { Chatwoot, configService } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { inbox } from '@figuro/chatwoot-sdk';
|
||||
import { Chatwoot as ChatwootModel, Contact, Message } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
import { proto } from 'baileys';
|
||||
|
||||
type ChatwootUser = {
|
||||
@ -209,6 +210,7 @@ class ChatwootImport {
|
||||
throw new Error('User not found to import messages.');
|
||||
}
|
||||
|
||||
const touchedConversations = new Set<string>();
|
||||
let totalMessagesImported = 0;
|
||||
|
||||
let messagesOrdered = this.historyMessages.get(instance.instanceName) || [];
|
||||
@ -284,6 +286,11 @@ class ChatwootImport {
|
||||
phoneNumbersWithTimestamp,
|
||||
messagesByPhoneNumber,
|
||||
);
|
||||
|
||||
for (const { conversation_id } of fksByNumber.values()) {
|
||||
touchedConversations.add(conversation_id);
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`[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})),`;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
if (bindInsertMsg.length > 2) {
|
||||
if (sqlInsertMsg.slice(-1) === ',') {
|
||||
sqlInsertMsg = sqlInsertMsg.slice(0, -1);
|
||||
@ -354,10 +363,24 @@ class ChatwootImport {
|
||||
|
||||
this.deleteHistoryMessages(instance);
|
||||
this.deleteRepositoryMessagesCache(instance);
|
||||
|
||||
|
||||
|
||||
this.logger.info(
|
||||
`[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 = {
|
||||
...provider,
|
||||
ignoreJids: Array.isArray(provider.ignoreJids) ? provider.ignoreJids.map((event) => String(event)) : [],
|
||||
@ -719,6 +742,42 @@ class ChatwootImport {
|
||||
|
||||
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();
|
||||
|
Loading…
Reference in New Issue
Block a user