mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-22 03:56:54 -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 && \
|
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
4
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
|
Loading…
Reference in New Issue
Block a user