Merge pull request #1504 from KokeroO/develop

fix: Melhora o método createConversation (evita conversas criadas duplicadas Chatwoot)
This commit is contained in:
Davidson Gomes 2025-05-27 08:11:34 -03:00 committed by GitHub
commit dd0dfd447c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 181 additions and 176 deletions

View File

@ -543,215 +543,220 @@ export class ChatwootService {
} }
public async createConversation(instance: InstanceDto, body: any) { public async createConversation(instance: InstanceDto, body: any) {
const remoteJid = body.key.remoteJid;
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
const maxWaitTime = 5000; // 5 secounds
try { try {
this.logger.verbose('--- Start createConversation ---'); this.logger.verbose(`--- Start createConversation ---`);
this.logger.verbose(`Instance: ${JSON.stringify(instance)}`); this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
const client = await this.clientCw(instance); // If it already exists in the cache, return conversationId
if (!client) {
this.logger.warn(`Client not found for instance: ${JSON.stringify(instance)}`);
return null;
}
const cacheKey = `${instance.instanceName}:createConversation-${body.key.remoteJid}`;
this.logger.verbose(`Cache key: ${cacheKey}`);
if (await this.cache.has(cacheKey)) { if (await this.cache.has(cacheKey)) {
this.logger.verbose(`Cache hit for key: ${cacheKey}`);
const conversationId = (await this.cache.get(cacheKey)) as number; const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Cached conversation ID: ${conversationId}`); this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
let conversationExists: conversation | boolean;
try {
conversationExists = await client.conversations.get({
accountId: this.provider.accountId,
conversationId: conversationId,
});
this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`);
} catch (error) {
this.logger.error(`Error getting conversation: ${error}`);
conversationExists = false;
}
if (!conversationExists) {
this.logger.verbose('Conversation does not exist, re-calling createConversation');
this.cache.delete(cacheKey);
return await this.createConversation(instance, body);
}
return conversationId; return conversationId;
} }
const isGroup = body.key.remoteJid.includes('@g.us'); // If lock already exists, wait until release or timeout
this.logger.verbose(`Is group: ${isGroup}`); if (await this.cache.has(lockKey)) {
this.logger.verbose(`Operação de criação já em andamento para ${remoteJid}, aguardando resultado...`);
const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; const start = Date.now();
this.logger.verbose(`Chat ID: ${chatId}`); while (await this.cache.has(lockKey)) {
if (Date.now() - start > maxWaitTime) {
let nameContact: string; this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
break;
nameContact = !body.key.fromMe ? body.pushName : chatId; }
this.logger.verbose(`Name contact: ${nameContact}`); await new Promise((res) => setTimeout(res, 300));
if (await this.cache.has(cacheKey)) {
const filterInbox = await this.getInbox(instance); const conversationId = (await this.cache.get(cacheKey)) as number;
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
if (!filterInbox) { return conversationId;
this.logger.warn(`Inbox not found for instance: ${JSON.stringify(instance)}`); }
return null; }
} }
if (isGroup) { // Adquire lock
this.logger.verbose('Processing group conversation'); await this.cache.set(lockKey, true, 30);
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId); this.logger.verbose(`Bloqueio adquirido para: ${lockKey}`);
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
nameContact = `${group.subject} (GROUP)`; try {
/*
Double check after lock
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
*/
if (await this.cache.has(cacheKey)) {
return (await this.cache.get(cacheKey)) as number;
}
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture( const client = await this.clientCw(instance);
body.key.participant.split('@')[0], if (!client) return null;
);
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]); const isGroup = remoteJid.includes('@g.us');
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`); const chatId = isGroup ? remoteJid : remoteJid.split('@')[0];
let nameContact = !body.key.fromMe ? body.pushName : chatId;
const filterInbox = await this.getInbox(instance);
if (!filterInbox) return null;
if (findParticipant) { if (isGroup) {
if (!findParticipant.name || findParticipant.name === chatId) { this.logger.verbose(`Processing group conversation`);
await this.updateContact(instance, findParticipant.id, { const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
name: body.pushName, this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
avatar_url: picture_url.profilePictureUrl || null,
}); nameContact = `${group.subject} (GROUP)`;
}
} else { const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
await this.createContact(
instance,
body.key.participant.split('@')[0], body.key.participant.split('@')[0],
filterInbox.id,
false,
body.pushName,
picture_url.profilePictureUrl || null,
body.key.participant,
); );
} this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
}
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`); this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
let contact = await this.findContact(instance, chatId); if (findParticipant) {
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`); if (!findParticipant.name || findParticipant.name === chatId) {
await this.updateContact(instance, findParticipant.id, {
if (contact) { name: body.pushName,
if (!body.key.fromMe) { avatar_url: picture_url.profilePictureUrl || null,
const waProfilePictureFile = });
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || ''; }
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || ''; } else {
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile; await this.createContact(
const nameNeedsUpdate = instance,
!contact.name || body.key.participant.split('@')[0],
contact.name === chatId || filterInbox.id,
(`+${chatId}`.startsWith('+55') false,
? this.getNumbers(`+${chatId}`).some( body.pushName,
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1), picture_url.profilePictureUrl || null,
) body.key.participant,
: false); );
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
if (pictureNeedsUpdate || nameNeedsUpdate) {
contact = await this.updateContact(instance, contact.id, {
...(nameNeedsUpdate && { name: nameContact }),
...(waProfilePictureFile === '' && { avatar: null }),
...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }),
});
} }
} }
} else {
const jid = body.key.remoteJid;
contact = await this.createContact(
instance,
chatId,
filterInbox.id,
isGroup,
nameContact,
picture_url.profilePictureUrl || null,
jid,
);
}
if (!contact) { const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
this.logger.warn('Contact not created or found'); this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
return null;
}
const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; let contact = await this.findContact(instance, chatId);
this.logger.verbose(`Contact ID: ${contactId}`);
const contactConversations = (await client.contacts.listConversations({ if (contact) {
accountId: this.provider.accountId, this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
id: contactId, if (!body.key.fromMe) {
})) as any; const waProfilePictureFile =
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`); picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
const nameNeedsUpdate =
!contact.name ||
contact.name === chatId ||
(`+${chatId}`.startsWith('+55')
? this.getNumbers(`+${chatId}`).some(
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
)
: false);
if (!contactConversations || !contactConversations.payload) { this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
this.logger.error('No conversations found or payload is undefined'); this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
return null;
}
let inboxConversation = contactConversations.payload.find( if (pictureNeedsUpdate || nameNeedsUpdate) {
(conversation) => conversation.inbox_id == filterInbox.id, contact = await this.updateContact(instance, contact.id, {
); ...(nameNeedsUpdate && { name: nameContact }),
if (inboxConversation) { ...(waProfilePictureFile === '' && { avatar: null }),
if (this.provider.reopenConversation) { ...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }),
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`); });
}
if (this.provider.conversationPending && inboxConversation.status !== 'open') {
await client.conversations.toggleStatus({
accountId: this.provider.accountId,
conversationId: inboxConversation.id,
data: {
status: 'pending',
},
});
} }
} else { } else {
inboxConversation = contactConversations.payload.find( const jid = body.key.remoteJid;
(conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, contact = await this.createContact(
instance,
chatId,
filterInbox.id,
isGroup,
nameContact,
picture_url.profilePictureUrl || null,
jid,
); );
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
} }
if (!contact) {
this.logger.warn(`Contact not created or found`);
return null;
}
const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id;
this.logger.verbose(`Contact ID: ${contactId}`);
const contactConversations = (await client.contacts.listConversations({
accountId: this.provider.accountId,
id: contactId,
})) as any;
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
if (!contactConversations || !contactConversations.payload) {
this.logger.error(`No conversations found or payload is undefined`);
return null;
}
let inboxConversation = contactConversations.payload.find(
(conversation) => conversation.inbox_id == filterInbox.id,
);
if (inboxConversation) { if (inboxConversation) {
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`); if (this.provider.reopenConversation) {
this.cache.set(cacheKey, inboxConversation.id); this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
return inboxConversation.id;
if (this.provider.conversationPending && inboxConversation.status !== 'open') {
await client.conversations.toggleStatus({
accountId: this.provider.accountId,
conversationId: inboxConversation.id,
data: {
status: 'pending',
},
});
}
} else {
inboxConversation = contactConversations.payload.find(
(conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
);
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
}
if (inboxConversation) {
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
this.cache.set(cacheKey, inboxConversation.id);
return inboxConversation.id;
}
} }
const data = {
contact_id: contactId.toString(),
inbox_id: filterInbox.id.toString(),
};
if (this.provider.conversationPending) {
data['status'] = 'pending';
}
const conversation = await client.conversations.create({
accountId: this.provider.accountId,
data,
});
if (!conversation) {
this.logger.warn(`Conversation not created or found`);
return null;
}
this.logger.verbose(`New conversation created with ID: ${conversation.id}`);
this.cache.set(cacheKey, conversation.id);
return conversation.id;
} finally {
await this.cache.delete(lockKey);
this.logger.verbose(`Block released for: ${lockKey}`);
} }
const data = {
contact_id: contactId.toString(),
inbox_id: filterInbox.id.toString(),
};
if (this.provider.conversationPending) {
data['status'] = 'pending';
}
const conversation = await client.conversations.create({
accountId: this.provider.accountId,
data,
});
if (!conversation) {
this.logger.warn('Conversation not created or found');
return null;
}
this.logger.verbose(`New conversation created with ID: ${conversation.id}`);
this.cache.set(cacheKey, conversation.id);
return conversation.id;
} catch (error) { } catch (error) {
this.logger.error(`Error in createConversation: ${error}`); this.logger.error(`Error in createConversation: ${error}`);
return null;
} }
} }

View File

@ -31,8 +31,8 @@ export class RabbitmqController extends EventController implements EventControll
port: url.port || 5672, port: url.port || 5672,
username: url.username || 'guest', username: url.username || 'guest',
password: url.password || 'guest', password: url.password || 'guest',
vhost: url.pathname.slice(1) || '/', vhost: url.pathname.slice(1) || '/',
frameMax: frameMax frameMax: frameMax,
}; };
amqp.connect(connectionOptions, (error, connection) => { amqp.connect(connectionOptions, (error, connection) => {